Compare commits

..

29 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
daniel
bdd334b6fb feat: add user-configurable MCP connections
- manage_mcp_connection tool: add/remove/enable/disable/list MCP servers
- mcp_loader: dynamic connection with OAuth/bearer/none auth, token caching
- Secrets stored in SSM, never in DynamoDB
- MCP clients loaded per-session and cleaned up in finally block
2026-05-13 21:55:01 -05:00
daniel
74f74ef877 refactor: migrate Secrets Manager secrets to SSM Parameter Store (free tier) 2026-05-13 12:55:16 -05:00
daniel
3a34e61479 feat: add windowed session history + LTM extraction/retrieval
- New memory_manager.py with:
  - check_and_compact: runs compaction on flagged sessions (extracts LTM via
    Claude Haiku, stores as AgentCore Memory event, deletes old events)
  - check_window_and_flag: sets DynamoDB flag when session > 100 events
  - load_ltm: retrieves LTM extractions and formats as system prompt block
- Wired into main.py:
  - Compaction runs before session_manager creation (trims old events)
  - LTM block injected into system prompt
  - Window check runs after session close
- SESSION_WINDOW_SIZE = 100 (named constant)
- Compaction is idempotent (uses event timestamps as cursor)
- LTM retrieval failure is non-fatal (logs and continues)
2026-05-13 11:57:50 -05:00
daniel
d217842917 refactor: remove MEMORY.md from prompt, add AgentCore memory instructions 2026-05-13 11:48:53 -05:00
daniel
3cc90550b5 feat: add Telegram file attachment support (inbound + outbound)
Inbound:
- tg-ingest detects document/photo/audio/video/voice attachments
- Downloads files via Telegram Bot API (getFile + download)
- Inlines small text files (<50KB) directly in the prompt
- Stores binary/large files to S3 (attachments/{chat_id}/{update_id}/{filename})
- agent-runner appends file context to the AgentCore prompt

Outbound:
- New send_file tool for the agent to send documents back to users
- TelegramAdapter.send_document uses multipart/form-data POST
- CDK grants tg-ingest S3 write access and passes bucket name env var
2026-05-13 05:34:33 -05:00
daniel
eba4f7db25 fix: align run_code with AWS docs pattern (invoke+executeCode, not execute_code wrapper) 2026-05-12 15:26:01 -05:00
daniel
9253d5046f feat: re-enable code interpreter tool (lazy code_session, no module-level init) 2026-05-12 15:05:26 -05:00
daniel
138f9224c3 fix: use sender from_id as actor_id in groups, not group chat_id 2026-05-12 14:17:02 -05:00
47 changed files with 3194 additions and 743 deletions

View File

@@ -20,9 +20,10 @@
"OAUTH_START_URL": "https://sptejrymri.execute-api.us-east-1.amazonaws.com/oauth/start", "OAUTH_START_URL": "https://sptejrymri.execute-api.us-east-1.amazonaws.com/oauth/start",
"USERS_TABLE_NAME": "agent-claw-users", "USERS_TABLE_NAME": "agent-claw-users",
"WORKSPACE_BUCKET_NAME": "agent-claw-workspace-495395224548", "WORKSPACE_BUCKET_NAME": "agent-claw-workspace-495395224548",
"TELEGRAM_BOT_TOKEN_SECRET_ARN": "arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/telegram-bot-token-Oq3in3", "TELEGRAM_BOT_TOKEN_SSM_PARAM": "/agent-claw/telegram-bot-token",
"BRAVE_API_KEY_SECRET_ARN": "arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/brave-api-key-uUSgzi", "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"
} }
} }
], ],

View File

@@ -19,14 +19,14 @@ class TelegramAdapter:
if self._token is None: if self._token is None:
with self._lock: with self._lock:
if self._token is None: if self._token is None:
secret_arn = self._secret_arn or os.environ.get( param_name = self._secret_arn or os.environ.get(
'TELEGRAM_BOT_TOKEN_SECRET_ARN', 'TELEGRAM_BOT_TOKEN_SSM_PARAM',
'arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/telegram-bot-token-Oq3in3' '/agent-claw/telegram-bot-token'
) )
sm = boto3.client('secretsmanager') ssm = boto3.client('ssm')
self._token = sm.get_secret_value( self._token = ssm.get_parameter(
SecretId=secret_arn Name=param_name, WithDecryption=True
)['SecretString'] )['Parameter']['Value']
return self._token return self._token
def _api(self, method: str, data: dict) -> dict: def _api(self, method: str, data: dict) -> dict:
@@ -63,6 +63,43 @@ class TelegramAdapter:
import traceback import traceback
print(f'[telegram] send_typing failed: {e}\n{traceback.format_exc()}') print(f'[telegram] send_typing failed: {e}\n{traceback.format_exc()}')
def send_document(self, file_bytes: bytes, filename: str, caption: str = '') -> str:
"""Send a file as a Telegram document using multipart/form-data. Returns message_id."""
import io
token = self._get_token()
url = f'https://api.telegram.org/bot{token}/sendDocument'
boundary = '----AgentClawBoundary'
body = io.BytesIO()
def add_field(name: str, value: str):
body.write(f'--{boundary}\r\n'.encode())
body.write(f'Content-Disposition: form-data; name="{name}"\r\n\r\n'.encode())
body.write(f'{value}\r\n'.encode())
def add_file(name: str, fname: str, data: bytes):
body.write(f'--{boundary}\r\n'.encode())
body.write(f'Content-Disposition: form-data; name="{name}"; filename="{fname}"\r\n'.encode())
body.write(b'Content-Type: application/octet-stream\r\n\r\n')
body.write(data)
body.write(b'\r\n')
add_field('chat_id', self.chat_id)
if self.thread_id is not None:
add_field('message_thread_id', str(self.thread_id))
if caption:
add_field('caption', caption)
add_file('document', filename, file_bytes)
body.write(f'--{boundary}--\r\n'.encode())
req = urllib.request.Request(
url, data=body.getvalue(),
headers={'Content-Type': f'multipart/form-data; boundary={boundary}'},
)
with urllib.request.urlopen(req, timeout=60) as resp:
result = json.loads(resp.read())
return str(result.get('result', {}).get('message_id', ''))
def edit(self, message_id: str, text: str) -> None: def edit(self, message_id: str, text: str) -> None:
"""Edit an existing message in-place.""" """Edit an existing message in-place."""
try: try:

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,12 +4,15 @@ agent-claw Runtime 1 — main assistant agent.
Entrypoint for AgentCore CodeZip deployment. Entrypoint for AgentCore CodeZip deployment.
""" """
import os import os
import time
from strands import Agent, tool from strands import Agent, tool
from strands.models import BedrockModel from strands.models import BedrockModel
from bedrock_agentcore.runtime import BedrockAgentCoreApp from bedrock_agentcore.runtime import BedrockAgentCoreApp
from channels.telegram import TelegramAdapter from channels.telegram import TelegramAdapter
from prompt_builder import build_system_prompt, invalidate_prompt from prompt_builder import build_system_prompt, invalidate_prompt
import memory_manager
import config
from tools import web as web_tools from tools import web as web_tools
from tools import workspace as ws_tools from tools import workspace as ws_tools
from tools import messaging from tools import messaging
@@ -17,7 +20,11 @@ from tools.scheduler import schedule_reminder, list_reminders, cancel_reminder
import tools.scheduler as _scheduler_module import tools.scheduler as _scheduler_module
from tools.home_assistant import home_assistant, set_ha_config from tools.home_assistant import home_assistant, set_ha_config
from tools.google_workspace import list_calendars, get_calendar_events, list_gmail_messages, get_gmail_message from tools.google_workspace import list_calendars, get_calendar_events, list_gmail_messages, get_gmail_message
from tools.send_file import send_file as _send_file_impl
from tools.mcp_tools import manage_mcp_connection
import tools.mcp_tools as _mcp_tools_module
import tools.google_workspace as _gws import tools.google_workspace as _gws
import mcp_loader
import httpx import httpx
import botocore.auth import botocore.auth
import botocore.awsrequest import botocore.awsrequest
@@ -29,6 +36,7 @@ OAUTH_START_URL = (
or 'https://sptejrymri.execute-api.us-east-1.amazonaws.com/oauth/start' or 'https://sptejrymri.execute-api.us-east-1.amazonaws.com/oauth/start'
) )
USERS_TABLE_NAME = os.environ.get('USERS_TABLE_NAME', 'agent-claw-users') 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): class _SigV4HttpxAuth(httpx.Auth):
@@ -56,12 +64,68 @@ class _SigV4HttpxAuth(httpx.Auth):
if self._actor_id: if self._actor_id:
request.headers['x-actor-id'] = self._actor_id request.headers['x-actor-id'] = self._actor_id
yield request yield request
from bedrock_agentcore.memory.integrations.strands.config import AgentCoreMemoryConfig from bedrock_agentcore.memory.integrations.strands.config import AgentCoreMemoryConfig
from bedrock_agentcore.memory.integrations.strands.session_manager import AgentCoreMemorySessionManager from bedrock_agentcore.memory.integrations.strands.session_manager import AgentCoreMemorySessionManager
# code_interpreter removed — causes [Errno 98] port 8080 conflict on warm container re-init # 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() 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 ────────────────────────────────────────────────────── # ── Tool definitions ──────────────────────────────────────────────────────
@@ -73,6 +137,19 @@ def web_search(query: str) -> str:
return web_tools.brave_search(query) return web_tools.brave_search(query)
@tool
def send_file(file_content: str, filename: str, caption: str = '') -> str:
"""Send a file to the user as a Telegram document attachment.
Use this when you need to send code, data, or any text content as a downloadable file.
Args:
file_content: The text content of the file to send.
filename: The filename with extension (e.g. 'report.txt', 'data.csv', 'script.py').
caption: Optional caption to display with the file.
"""
return _send_file_impl(file_content, filename, caption)
@tool @tool
def web_fetch(url: str) -> str: def web_fetch(url: str) -> str:
"""Fetch and extract readable text content from a URL.""" """Fetch and extract readable text content from a URL."""
@@ -244,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".' 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 ──────────────────────────────────────────────────────────── # ── Entrypoint ────────────────────────────────────────────────────────────
# Module-level actor_id for tool closures (set per-invocation) # Module-level actor_id for tool closures (set per-invocation)
@@ -297,6 +605,10 @@ async def main(payload: dict, context):
_current_chat_id = chat_id _current_chat_id = chat_id
_scheduler_module._current_actor_id = actor_id _scheduler_module._current_actor_id = actor_id
_scheduler_module._current_chat_id = chat_id _scheduler_module._current_chat_id = chat_id
_mcp_tools_module._current_actor_id = actor_id
# Run compaction if flagged from previous invocation (trims old events before load)
memory_manager.check_and_compact(actor_id, session_id)
memory_config = AgentCoreMemoryConfig( memory_config = AgentCoreMemoryConfig(
memory_id=MEMORY_ID, memory_id=MEMORY_ID,
@@ -337,39 +649,83 @@ async def main(payload: dict, context):
user_context += f'\nEnrolled services: {", ".join(enrolled)}' user_context += f'\nEnrolled services: {", ".join(enrolled)}'
system_prompt = build_system_prompt(user_context=user_context, actor_id=actor_id) system_prompt = build_system_prompt(user_context=user_context, actor_id=actor_id)
# Inject current datetime so the model always has accurate time context # Inject long-term memory block before conversation history
from datetime import datetime ltm_block = memory_manager.load_ltm(actor_id)
from zoneinfo import ZoneInfo if ltm_block:
_tz = ZoneInfo('America/Chicago') system_prompt = system_prompt + '\n\n---\n\n' + ltm_block
_now = datetime.now(_tz)
_time_str = _now.strftime('%A, %B %d, %Y %I:%M %p %Z') 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 = system_prompt + f'\n\nCurrent date/time: {_time_str}' 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.'
print(f'[main] System prompt time injection: {_time_str}')
# Model: claude-sonnet-4-6 via cross-region inference # Model: claude-sonnet-4-6 via cross-region inference
# NOTE: extended thinking disabled — causes retry/duplicate issues with streaming # NOTE: extended thinking disabled — causes retry/duplicate issues with streaming
from botocore.config import Config as BotoConfig from botocore.config import Config as BotoConfig
model = BedrockModel( model = BedrockModel(
model_id="us.anthropic.claude-sonnet-4-6", model_id=config.AGENT_MODEL_ID,
region_name="us-east-1", region_name="us-east-1",
boto_client_config=BotoConfig(read_timeout=600, connect_timeout=10), boto_client_config=BotoConfig(read_timeout=600, connect_timeout=10),
) )
base_tools = [web_search, web_fetch, read_workspace_file, write_workspace_file, base_tools = [web_search, web_fetch, read_workspace_file, write_workspace_file,
home_assistant, connect_google_account, list_google_accounts, remove_google_account, home_assistant, connect_google_account, list_google_accounts, remove_google_account,
manage_service, schedule_reminder, list_reminders, cancel_reminder, manage_service, manage_mcp_connection, schedule_reminder, list_reminders, cancel_reminder,
list_calendars, get_calendar_events, list_gmail_messages, get_gmail_message] list_calendars, get_calendar_events, list_gmail_messages, get_gmail_message,
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)
# 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( agent = Agent(
model=model, model=model,
system_prompt=system_prompt, system_prompt=system_prompt,
session_manager=session_manager, session_manager=session_manager,
tools=base_tools, 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 final_message = None
try: try:
async for event in agent.stream_async(payload.get('prompt', '')): async for event in agent.stream_async(prompt):
if 'result' in event: if 'result' in event:
final_message = event['result'].message final_message = event['result'].message
yield event yield event
@@ -382,6 +738,9 @@ async def main(payload: dict, context):
finally: finally:
_typing_active = False _typing_active = False
session_manager.close() session_manager.close()
mcp_loader.close_mcp_clients(_mcp_to_close)
# Check if session exceeds window — flag for compaction on next invocation
memory_manager.check_window_and_flag(actor_id, session_id)
app.run() app.run()

View File

@@ -0,0 +1,122 @@
"""Dynamic MCP tool loader — connects to user-configured MCP servers and returns their tools."""
import time
import logging
import urllib.request
import urllib.parse
import json
import boto3
from mcp.client.streamable_http import streamablehttp_client
from strands.tools.mcp.mcp_client import MCPClient
logger = logging.getLogger(__name__)
# Token cache: {f"{actor_id}:{conn_name}": {"token": str, "expires_at": float}}
_token_cache: dict = {}
def _get_ssm_value(param_name: str) -> str:
ssm = boto3.client('ssm', region_name='us-east-1')
return ssm.get_parameter(Name=param_name, WithDecryption=True)['Parameter']['Value']
def _get_oauth_token(conn: dict, actor_id: str) -> str:
"""Fetch OAuth token via client_credentials grant, with caching."""
cache_key = f"{actor_id}:{conn['name']}"
cached = _token_cache.get(cache_key)
if cached and cached['expires_at'] > time.time():
return cached['token']
client_secret = _get_ssm_value(conn['client_secret_ssm'])
data = urllib.parse.urlencode({
'grant_type': 'client_credentials',
'client_id': conn['client_id'],
'client_secret': client_secret,
'scope': conn.get('scope', ''),
}).encode()
req = urllib.request.Request(conn['cognito_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 - 30}
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}'}
return {}
def invalidate_token(conn_name: str, actor_id: str):
"""Invalidate cached token for a connection (call on auth failure)."""
_token_cache.pop(f"{actor_id}:{conn_name}", None)
def load_mcp_tools(mcp_connections: list, actor_id: str) -> tuple[list, list]:
"""Connect to each enabled MCP server and return (tools_list, clients_to_close).
Returns:
Tuple of (list of MCPClient instances to pass to Agent, list of same clients to close later)
"""
clients = []
for conn in mcp_connections:
if not conn.get('enabled', True):
continue
name = conn.get('name', 'unknown')
try:
headers = _resolve_auth_headers(conn, actor_id)
url = conn['url']
client = MCPClient(lambda u=url, h=headers: streamablehttp_client(u, headers=h))
client.start()
clients.append(client)
logger.info(f'[mcp_loader] Connected to MCP server: {name}')
except Exception as e:
logger.error(f'[mcp_loader] Failed to connect to MCP server "{name}": {e}')
return clients, clients
def close_mcp_clients(clients: list):
"""Close all MCP clients."""
for client in clients:
try:
client.stop()
except Exception as e:
logger.error(f'[mcp_loader] Error closing MCP client: {e}')

View File

@@ -0,0 +1,321 @@
"""Long-term memory manager: windowed loading, compaction, and LTM retrieval."""
import json
import logging
import os
from datetime import datetime, timezone
from typing import Any
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'
_memory_client: MemoryClient | None = None
def _get_memory_client() -> MemoryClient:
global _memory_client
if _memory_client is None:
_memory_client = MemoryClient(region_name='us-east-1')
return _memory_client
def _get_compaction_flag(actor_id: str) -> bool:
"""Check if compaction is needed for this actor."""
ddb = boto3.resource('dynamodb', region_name='us-east-1')
table = ddb.Table(USERS_TABLE_NAME)
resp = table.get_item(Key={'actor_id': actor_id})
return resp.get('Item', {}).get('needs_compaction', False)
def _set_compaction_flag(actor_id: str, value: bool) -> None:
"""Set or clear the compaction flag."""
ddb = boto3.resource('dynamodb', region_name='us-east-1')
table = ddb.Table(USERS_TABLE_NAME)
table.update_item(
Key={'actor_id': actor_id},
UpdateExpression='SET needs_compaction = :v',
ExpressionAttributeValues={':v': value},
)
def _count_session_events(actor_id: str, session_id: str) -> int:
"""Count total events in the session (excluding state/agent metadata events)."""
client = _get_memory_client()
events = client.list_events(
memory_id=MEMORY_ID,
actor_id=actor_id,
session_id=session_id,
max_results=10000,
include_payload=False,
)
# Filter out session/agent state events (they have stateType metadata)
return sum(1 for e in events if not e.get('metadata', {}).get('stateType'))
def _get_all_session_events(actor_id: str, session_id: str) -> list[dict]:
"""Get all conversation events (excluding state metadata events)."""
client = _get_memory_client()
events = client.list_events(
memory_id=MEMORY_ID,
actor_id=actor_id,
session_id=session_id,
max_results=10000,
include_payload=True,
)
return [e for e in events if not e.get('metadata', {}).get('stateType')]
def _extract_text_from_events(events: list[dict]) -> str:
"""Extract conversation text from events for summarization."""
lines = []
for event in events:
for item in event.get('payload', []):
if 'conversational' in item:
conv = item['conversational']
role = conv.get('role', 'UNKNOWN')
text = conv.get('content', {}).get('text', '')
lines.append(f'{role}: {text}')
elif 'blob' in item:
try:
blob = json.loads(item['blob']) if isinstance(item['blob'], str) else item['blob']
if isinstance(blob, list) and blob:
for msg in blob:
if isinstance(msg, (list, tuple)) and len(msg) == 2:
lines.append(f'{msg[1]}: {msg[0]}')
except (json.JSONDecodeError, TypeError):
pass
return '\n'.join(lines[-200:]) # Cap at last 200 lines to stay within context
def _call_claude_extraction(conversation_text: str) -> dict:
"""Call Claude Haiku to extract structured LTM from conversation text."""
bedrock = boto3.client('bedrock-runtime', region_name='us-east-1')
prompt = (
'Extract structured long-term memory from this conversation. '
'Return ONLY valid JSON with these keys:\n'
'- "summary": 3-5 sentence narrative of what was discussed\n'
'- "facts": array of factual statements worth remembering\n'
'- "preferences": array of user preferences expressed\n'
'- "dates": array of events/deadlines with date/time mentioned\n'
'- "topics": array of topic keywords\n\n'
'Conversation:\n' + conversation_text
)
resp = bedrock.converse(
modelId=config.COMPACTION_MODEL_ID,
messages=[{'role': 'user', 'content': [{'text': prompt}]}],
inferenceConfig={'maxTokens': 1024},
)
text = resp['output']['message']['content'][0]['text']
# Parse JSON from response (handle markdown code blocks)
if '```' in text:
text = text.split('```')[1]
if text.startswith('json'):
text = text[4:]
return json.loads(text.strip())
def _get_last_compaction_timestamp(actor_id: str) -> str | None:
"""Get the timestamp of the most recent LTM extraction to avoid duplicates."""
client = _get_memory_client()
events = client.list_events(
memory_id=MEMORY_ID,
actor_id=actor_id,
session_id=LTM_SESSION_ID,
event_metadata=[{
'left': {'metadataKey': 'type'},
'operator': 'EQUALS_TO',
'right': {'metadataValue': {'stringValue': 'ltm_extraction'}},
}],
max_results=1,
include_payload=False,
)
if events:
# Events are returned chronologically; last one is most recent
return str(events[-1].get('eventTimestamp', ''))
return None
def check_and_compact(actor_id: str, session_id: str) -> None:
"""Run compaction if the flag is set. Call BEFORE creating session_manager."""
if not _get_compaction_flag(actor_id):
return
logger.info('[memory_manager] Compaction triggered for actor_id=%s', actor_id)
try:
events = _get_all_session_events(actor_id, session_id)
total = len(events)
if total <= SESSION_WINDOW_SIZE:
_set_compaction_flag(actor_id, False)
return
# Events to compact: everything before the window
compact_count = total - SESSION_WINDOW_SIZE
events_to_compact = events[:compact_count]
# Idempotency: check if we already compacted up to this timestamp
last_compacted = _get_last_compaction_timestamp(actor_id)
oldest_event_ts = str(events_to_compact[-1].get('eventTimestamp', ''))
if last_compacted and last_compacted >= oldest_event_ts:
logger.info('[memory_manager] Already compacted up to %s, skipping', last_compacted)
_set_compaction_flag(actor_id, False)
return
# Extract text and call Claude
text = _extract_text_from_events(events_to_compact)
if not text.strip():
logger.info('[memory_manager] No text to compact, clearing flag')
_set_compaction_flag(actor_id, False)
return
extraction = _call_claude_extraction(text)
# Store LTM extraction as an event
client = _get_memory_client()
client.create_event(
memory_id=MEMORY_ID,
actor_id=actor_id,
session_id=LTM_SESSION_ID,
messages=[(json.dumps(extraction), 'ASSISTANT')],
event_timestamp=datetime.now(timezone.utc),
metadata={
'type': {'stringValue': 'ltm_extraction'},
'actor_id': {'stringValue': actor_id},
'compacted_through': {'stringValue': oldest_event_ts},
},
)
# Delete compacted events from the session
for event in events_to_compact:
try:
client.gmdp_client.delete_event(
memoryId=MEMORY_ID,
actorId=actor_id,
sessionId=session_id,
eventId=event['eventId'],
)
except Exception as e:
logger.warning('[memory_manager] Failed to delete event %s: %s', event.get('eventId'), e)
_set_compaction_flag(actor_id, False)
logger.info('[memory_manager] Compacted %d events into LTM for actor_id=%s', compact_count, actor_id)
except Exception as e:
logger.error('[memory_manager] Compaction failed: %s', e)
# Don't clear flag — retry next invocation
def check_window_and_flag(actor_id: str, session_id: str) -> None:
"""After session loads, check if we exceed the window and set flag for next time."""
try:
count = _count_session_events(actor_id, session_id)
if count > SESSION_WINDOW_SIZE:
logger.info('[memory_manager] Session has %d events (> %d), setting compaction flag',
count, SESSION_WINDOW_SIZE)
_set_compaction_flag(actor_id, True)
except Exception as e:
logger.error('[memory_manager] Failed to check window: %s', e)
def load_ltm(actor_id: str) -> str:
"""Load all LTM extractions for an actor and format as a system prompt block.
Returns empty string on failure (non-fatal).
"""
try:
client = _get_memory_client()
events = client.list_events(
memory_id=MEMORY_ID,
actor_id=actor_id,
session_id=LTM_SESSION_ID,
event_metadata=[{
'left': {'metadataKey': 'type'},
'operator': 'EQUALS_TO',
'right': {'metadataValue': {'stringValue': 'ltm_extraction'}},
}],
max_results=50,
include_payload=True,
)
if not events:
return ''
# Parse extractions (events are chronological, reverse for most-recent-first)
extractions = []
for event in reversed(events):
for item in event.get('payload', []):
if 'conversational' in item:
text = item['conversational'].get('content', {}).get('text', '')
try:
extractions.append(json.loads(text))
except (json.JSONDecodeError, TypeError):
pass
if not extractions:
return ''
# Build the LTM block: most recent summary first, then deduplicated lists
parts = ['## Long-term memory\n']
# Most recent summary
if extractions[0].get('summary'):
parts.append(f'**Recent context:** {extractions[0]["summary"]}\n')
# Deduplicated facts
all_facts = []
seen_facts: set[str] = set()
for ext in extractions:
for f in ext.get('facts', []):
key = f.lower().strip()
if key not in seen_facts:
seen_facts.add(key)
all_facts.append(f)
if all_facts:
parts.append('**Facts:**')
for f in all_facts[:30]: # Cap at 30
parts.append(f'- {f}')
parts.append('')
# Deduplicated preferences
all_prefs = []
seen_prefs: set[str] = set()
for ext in extractions:
for p in ext.get('preferences', []):
key = p.lower().strip()
if key not in seen_prefs:
seen_prefs.add(key)
all_prefs.append(p)
if all_prefs:
parts.append('**Preferences:**')
for p in all_prefs[:15]:
parts.append(f'- {p}')
parts.append('')
# Dates (most recent extractions first, keep all)
all_dates = []
for ext in extractions:
all_dates.extend(ext.get('dates', []))
if all_dates:
parts.append('**Upcoming dates/events:**')
for d in all_dates[:10]:
parts.append(f'- {d}')
parts.append('')
result = '\n'.join(parts)
logger.info('[memory_manager] Loaded LTM block: %d chars from %d extractions', len(result), len(extractions))
return result
except Exception as e:
logger.error('[memory_manager] LTM retrieval failed (non-fatal): %s', e)
return ''

View File

@@ -46,35 +46,50 @@ def _get_base_prompt(actor_id: str = '') -> str:
s3 = boto3.client('s3') s3 = boto3.client('s3')
parts = [] parts = []
# Per-user MEMORY.md (falls back to global) # Inject active goal at the top of context
memory_key = f'users/{actor_id}/MEMORY.md' if actor_id else 'MEMORY.md'
try: try:
obj = s3.get_object(Bucket=bucket, Key=memory_key) obj = s3.get_object(Bucket=bucket, Key='GOAL.md')
content = obj['Body'].read().decode('utf-8') goal_content = obj['Body'].read().decode('utf-8')
parts.append(f'## MEMORY.md\n{content}') if '**Status:** active' in goal_content:
print(f'[prompt_builder] Loaded {memory_key} ({len(content)} bytes)') parts.append(f'## Active Goal\n{goal_content}')
except ClientError as e: print(f'[prompt_builder] Injected GOAL.md ({len(goal_content)} bytes)')
if e.response['Error']['Code'] in ('NoSuchKey', 'AccessDenied') and actor_id: except Exception:
# Fall back to global MEMORY.md pass
try:
obj = s3.get_object(Bucket=bucket, Key='MEMORY.md')
content = obj['Body'].read().decode('utf-8')
parts.append(f'## MEMORY.md\n{content}')
print(f'[prompt_builder] Loaded MEMORY.md (fallback, {len(content)} bytes)')
except Exception as e2:
print(f'[prompt_builder] Failed to load MEMORY.md: {e2}')
else:
print(f'[prompt_builder] Failed to load {memory_key}: {e}')
for fname in ['SOUL.md', 'AGENTS.md', 'IDENTITY.md', 'TOOLS.md', 'HEARTBEAT.md']: for fname in ['SOUL.md', 'STATUS.md']:
try: try:
obj = s3.get_object(Bucket=bucket, Key=fname) obj = s3.get_object(Bucket=bucket, Key=fname)
content = obj['Body'].read().decode('utf-8') 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)') print(f'[prompt_builder] Loaded {fname} ({len(content)} bytes)')
except Exception as e: except Exception as e:
print(f'[prompt_builder] Failed to load {fname}: {e}') print(f'[prompt_builder] Failed to load {fname}: {e}')
parts.append(
'## Memory\n'
'Your memory works through two layers:\n\n'
'**Conversation history (short-term):** AgentCore automatically loads your full '
'conversation history with this user at the start of each session. You have complete '
'context of everything discussed previously — no need to ask users to repeat themselves.\n\n'
'**Long-term facts (LTM):** Important facts extracted from past conversations are '
'retrieved and injected as context automatically. These are things like preferences, '
'setup details, names, and recurring topics the user has shared.\n\n'
'Guidelines:\n'
'- Never ask "what did we discuss last time?" — you already have the history.\n'
'- When a user shares something important (job interview, preference, key decision, '
'setup change), acknowledge it and trust it will be captured — do not ask if they '
'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.\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') parts.append('## Runtime\nRuntime: agent-claw | host=AgentCore | model=bedrock-claude-sonnet | channel=telegram | timezone=America/Chicago')
result = '\n\n---\n\n'.join(parts) result = '\n\n---\n\n'.join(parts)

View File

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

View File

@@ -1,68 +1,42 @@
"""Code interpreter tool — runs Python code in AgentCore managed sandbox.""" """Code interpreter tool — runs Python code in AgentCore managed sandbox.
Follows the AWS-recommended pattern:
https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/code-interpreter-building-agents.html
"""
import json
import os import os
import base64
from strands import tool from strands import tool
from bedrock_agentcore.tools.code_interpreter_client import code_session
def _parse_stream(result: dict) -> str:
"""Parse the streaming response from invoke_code_interpreter."""
parts = []
if "stream" not in result:
return str(result)
for event in result["stream"]:
if "result" not in event:
continue
for item in event["result"].get("content", []):
item_type = item.get("type", "")
if item_type == "text":
text = item.get("text", "")
if text:
parts.append(text)
elif item_type == "resource":
resource = item.get("resource", {})
if "text" in resource:
parts.append(resource["text"])
elif "blob" in resource:
try:
parts.append(base64.b64decode(resource["blob"]).decode("utf-8"))
except Exception:
parts.append(f"<binary resource: {resource.get('uri', '?')}>")
elif item_type == "image":
# Base64-encoded image
image_data = item.get("source", {}).get("data", "")
mime = item.get("source", {}).get("mediaType", "image/png")
parts.append(f"<image: {mime}, {len(image_data)} bytes base64>")
return "\n".join(parts) if parts else "(no output)"
@tool @tool
def run_code(code: str, packages: list[str] | None = None) -> str: def run_code(code: str, description: str = '') -> str:
"""Execute Python code in a secure managed sandbox and return the output. """Execute Python code in a secure AgentCore managed sandbox and return the output.
Optionally install pip packages before running (e.g. ['pandas', 'numpy']). State is maintained within a single session but not across separate calls.
Supports data analysis, calculations, file I/O, and most common Python libraries.
Args: Args:
code: Python code to execute. code: Python code to execute.
packages: Optional list of pip packages to install first. description: Optional description prepended as a comment.
Returns: Returns:
Execution output (stdout, results, errors). JSON result with stdout, stderr, exitCode, and executionTime.
""" """
try: if description:
from bedrock_agentcore.tools import CodeInterpreter, code_session code = f'# {description}\n{code}'
print(f'[run_code] executing {len(code)}c')
region = os.environ.get('AWS_REGION', 'us-east-1') region = os.environ.get('AWS_REGION', 'us-east-1')
with code_session(region) as client: with code_session(region) as client:
if packages: response = client.invoke('executeCode', {
install_raw = client.install_packages(packages) 'code': code,
install_out = _parse_stream(install_raw) if isinstance(install_raw, dict) else str(install_raw) 'language': 'python',
print(f'[code_interpreter] install: {install_out[:200]}') 'clearContext': False,
})
raw = client.execute_code(code) for event in response['stream']:
return _parse_stream(raw) return json.dumps(event['result'])
except Exception as e: return json.dumps({'isError': True, 'content': [{'type': 'text', 'text': 'No output from code interpreter'}]})
import traceback
return f'Code interpreter error: {type(e).__name__}: {e}\n{traceback.format_exc()[-500:]}'

View File

@@ -0,0 +1,148 @@
"""MCP connection management tool — add/remove/enable/disable user MCP servers."""
import boto3
from strands import tool
USERS_TABLE_NAME = 'agent-claw-users'
# Set per-invocation by main.py
_current_actor_id: str = ''
@tool
def manage_mcp_connection(action: str, name: str = '', url: str = '',
auth_type: str = 'none', cognito_token_url: str = '',
client_id: str = '', client_secret: str = '',
scope: str = '', token: str = '') -> str:
"""Add, remove, enable, disable, or list MCP server connections.
Actions: add, remove, enable, disable, list
For add with auth_type=oauth_client_credentials, provide:
- cognito_token_url: Cognito token endpoint
- client_id: OAuth client ID
- client_secret: Secret value (stored securely in SSM, not in database)
- scope: Space-separated OAuth scopes
For add with auth_type=bearer, provide:
- token: Bearer token value (stored securely in SSM, not in database)
For add with auth_type=none, only name and url are required.
Args:
action: One of "add", "remove", "enable", "disable", "list".
name: Connection name (required for add/remove/enable/disable).
url: MCP server URL (required for add).
auth_type: One of "none", "bearer", "oauth_client_credentials".
cognito_token_url: Token endpoint for oauth_client_credentials.
client_id: OAuth client ID for oauth_client_credentials.
client_secret: OAuth client secret (will be stored in SSM).
scope: OAuth scopes for oauth_client_credentials.
token: Bearer token (will be stored in SSM).
"""
actor_id = _current_actor_id
if not actor_id:
return 'Cannot determine actor_id.'
ddb = boto3.resource('dynamodb', region_name='us-east-1')
table = ddb.Table(USERS_TABLE_NAME)
if action == 'list':
resp = table.get_item(Key={'actor_id': actor_id})
connections = resp.get('Item', {}).get('services', {}).get('mcp_connections', [])
if not connections:
return 'No MCP connections configured.'
lines = []
for c in connections:
status = '' if c.get('enabled', True) else ''
lines.append(f" {status} {c['name']}: {c['url']} (auth: {c.get('auth_type', 'none')})")
return 'MCP connections:\n' + '\n'.join(lines)
if not name:
return 'name is required for this action.'
if action == 'add':
if not url:
return 'url is required for add.'
if auth_type not in ('none', 'bearer', 'oauth_client_credentials'):
return f'Invalid auth_type: {auth_type}. Use none, bearer, or oauth_client_credentials.'
ssm = boto3.client('ssm', region_name='us-east-1')
safe_actor = actor_id.replace(':', '-')
ssm_prefix = f'/agent-claw/mcp/{safe_actor}/{name}'
conn = {'name': name, 'url': url, 'auth_type': auth_type, 'enabled': True}
if auth_type == 'oauth_client_credentials':
if not cognito_token_url or not client_id or not client_secret:
return 'oauth_client_credentials requires cognito_token_url, client_id, and client_secret.'
ssm.put_parameter(Name=f'{ssm_prefix}/client-secret', Value=client_secret,
Type='SecureString', Overwrite=True)
conn['cognito_token_url'] = cognito_token_url
conn['client_id'] = client_id
conn['client_secret_ssm'] = f'{ssm_prefix}/client-secret'
conn['scope'] = scope
elif auth_type == 'bearer':
if not token:
return 'bearer auth requires token.'
ssm.put_parameter(Name=f'{ssm_prefix}/token', Value=token,
Type='SecureString', Overwrite=True)
conn['token_ssm'] = f'{ssm_prefix}/token'
# Upsert into mcp_connections list
resp = table.get_item(Key={'actor_id': actor_id})
services = resp.get('Item', {}).get('services', {})
connections = services.get('mcp_connections', [])
connections = [c for c in connections if c['name'] != name]
connections.append(conn)
table.update_item(
Key={'actor_id': actor_id},
UpdateExpression='SET services = if_not_exists(services, :empty), services.mcp_connections = :conns',
ExpressionAttributeValues={':conns': connections, ':empty': {}},
)
return f'MCP connection "{name}" added ({auth_type} auth). It will be available on your next message.'
elif action == 'remove':
resp = table.get_item(Key={'actor_id': actor_id})
connections = resp.get('Item', {}).get('services', {}).get('mcp_connections', [])
found = [c for c in connections if c['name'] == name]
if not found:
return f'No connection named "{name}" found.'
# Clean up SSM secrets
ssm = boto3.client('ssm', region_name='us-east-1')
safe_actor = actor_id.replace(':', '-')
for key in ('client-secret', 'token'):
try:
ssm.delete_parameter(Name=f'/agent-claw/mcp/{safe_actor}/{name}/{key}')
except ssm.exceptions.ParameterNotFound:
pass
connections = [c for c in connections if c['name'] != name]
table.update_item(
Key={'actor_id': actor_id},
UpdateExpression='SET services.mcp_connections = :conns',
ExpressionAttributeValues={':conns': connections},
)
return f'MCP connection "{name}" removed.'
elif action in ('enable', 'disable'):
resp = table.get_item(Key={'actor_id': actor_id})
connections = resp.get('Item', {}).get('services', {}).get('mcp_connections', [])
updated = False
for c in connections:
if c['name'] == name:
c['enabled'] = (action == 'enable')
updated = True
break
if not updated:
return f'No connection named "{name}" found.'
table.update_item(
Key={'actor_id': actor_id},
UpdateExpression='SET services.mcp_connections = :conns',
ExpressionAttributeValues={':conns': connections},
)
return f'MCP connection "{name}" {action}d.'
else:
return f'Unknown action: {action}. Use add, remove, enable, disable, or list.'

View File

@@ -0,0 +1,20 @@
"""Send file tool — sends documents to the user via Telegram."""
from tools import messaging
def send_file(file_content: str, filename: str, caption: str = '') -> str:
"""Send a file to the user as a Telegram document attachment.
Args:
file_content: The text content of the file to send.
filename: The filename (e.g. 'report.txt', 'data.csv').
caption: Optional caption to display with the file.
"""
adapter = messaging._adapter
if adapter is None:
return 'No channel adapter configured.'
if not hasattr(adapter, 'send_document'):
return 'Channel adapter does not support file sending.'
file_bytes = file_content.encode('utf-8')
msg_id = adapter.send_document(file_bytes, filename, caption)
return f'File "{filename}" sent (id={msg_id})' if msg_id else f'File "{filename}" sent'

View File

@@ -15,12 +15,12 @@ def _get_brave_key() -> str:
if _brave_key is None: if _brave_key is None:
with _brave_lock: with _brave_lock:
if _brave_key is None: if _brave_key is None:
secret_arn = os.environ.get( param_name = os.environ.get(
'BRAVE_API_KEY_SECRET_ARN', 'BRAVE_API_KEY_SSM_PARAM',
'arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/brave-api-key-uUSgzi' '/agent-claw/brave-api-key'
) )
sm = boto3.client('secretsmanager') ssm = boto3.client('ssm')
_brave_key = sm.get_secret_value(SecretId=secret_arn)['SecretString'] _brave_key = ssm.get_parameter(Name=param_name, WithDecryption=True)['Parameter']['Value']
return _brave_key return _brave_key

View File

@@ -15,6 +15,7 @@ dependencies = [
{ name = "aws-opentelemetry-distro" }, { name = "aws-opentelemetry-distro" },
{ name = "bedrock-agentcore" }, { name = "bedrock-agentcore" },
{ name = "botocore", extra = ["crt"] }, { name = "botocore", extra = ["crt"] },
{ name = "mcp-proxy-for-aws" },
{ name = "strands-agents" }, { name = "strands-agents" },
{ name = "strands-agents-tools" }, { name = "strands-agents-tools" },
{ name = "workspace-mcp" }, { name = "workspace-mcp" },
@@ -25,6 +26,7 @@ requires-dist = [
{ name = "aws-opentelemetry-distro" }, { name = "aws-opentelemetry-distro" },
{ name = "bedrock-agentcore", specifier = ">=1.0.3" }, { name = "bedrock-agentcore", specifier = ">=1.0.3" },
{ name = "botocore", extras = ["crt"], specifier = ">=1.35.0" }, { 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", specifier = ">=1.13.0" },
{ name = "strands-agents-tools", specifier = ">=0.5.0" }, { name = "strands-agents-tools", specifier = ">=0.5.0" },
{ name = "workspace-mcp", specifier = ">=1.20.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" }, { 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]] [[package]]
name = "mdurl" name = "mdurl"
version = "0.1.2" version = "0.1.2"

View File

@@ -5,6 +5,11 @@ import { AgentClawStack } from '../lib/agent-claw-stack';
const app = new cdk.App(); 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', { new AgentClawStack(app, 'AgentClawStack', {
env: { env: {
account: process.env.CDK_DEFAULT_ACCOUNT, account: process.env.CDK_DEFAULT_ACCOUNT,

View File

@@ -1,91 +1,46 @@
{ {
"version": "53.0.0", "version": "53.0.0",
"files": { "files": {
"e2659170a0721541efa761a8d5d04d5e36cbbf691c4b15a9053002b7c825055d": { "e0a834c0c682fe0c528540082d0b587c43ed791927a064104e5eb6c507c0cdb2": {
"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": {
"displayName": "TgIngest/Code", "displayName": "TgIngest/Code",
"source": { "source": {
"path": "asset.f4461651bfa7d2822e3f36525ace7882e1610dcdaf85e052e1907241e25491d6", "path": "asset.e0a834c0c682fe0c528540082d0b587c43ed791927a064104e5eb6c507c0cdb2",
"packaging": "zip" "packaging": "zip"
}, },
"destinations": { "destinations": {
"495395224548-us-east-1-2abe2e26": { "495395224548-us-east-1-351c433c": {
"bucketName": "cdk-hnb659fds-assets-495395224548-us-east-1", "bucketName": "cdk-hnb659fds-assets-495395224548-us-east-1",
"objectKey": "f4461651bfa7d2822e3f36525ace7882e1610dcdaf85e052e1907241e25491d6.zip", "objectKey": "e0a834c0c682fe0c528540082d0b587c43ed791927a064104e5eb6c507c0cdb2.zip",
"region": "us-east-1", "region": "us-east-1",
"assumeRoleArn": "arn:${AWS::Partition}:iam::495395224548:role/cdk-hnb659fds-file-publishing-role-495395224548-us-east-1" "assumeRoleArn": "arn:${AWS::Partition}:iam::495395224548:role/cdk-hnb659fds-file-publishing-role-495395224548-us-east-1"
} }
} }
}, },
"e8d92532d2cb081ba122764c803acc80aaa41350d3497665468ca165dd5ff799": { "59b4a808a6125020261c01ae23500d036cdde29fdb255aa6a82d61555fde4848": {
"displayName": "AgentRunner/Code", "displayName": "AgentRunner/Code",
"source": { "source": {
"path": "asset.e8d92532d2cb081ba122764c803acc80aaa41350d3497665468ca165dd5ff799", "path": "asset.59b4a808a6125020261c01ae23500d036cdde29fdb255aa6a82d61555fde4848",
"packaging": "zip" "packaging": "zip"
}, },
"destinations": { "destinations": {
"495395224548-us-east-1-7c682050": { "495395224548-us-east-1-16e7a6a4": {
"bucketName": "cdk-hnb659fds-assets-495395224548-us-east-1", "bucketName": "cdk-hnb659fds-assets-495395224548-us-east-1",
"objectKey": "e8d92532d2cb081ba122764c803acc80aaa41350d3497665468ca165dd5ff799.zip", "objectKey": "59b4a808a6125020261c01ae23500d036cdde29fdb255aa6a82d61555fde4848.zip",
"region": "us-east-1", "region": "us-east-1",
"assumeRoleArn": "arn:${AWS::Partition}:iam::495395224548:role/cdk-hnb659fds-file-publishing-role-495395224548-us-east-1" "assumeRoleArn": "arn:${AWS::Partition}:iam::495395224548:role/cdk-hnb659fds-file-publishing-role-495395224548-us-east-1"
} }
} }
}, },
"99aabce70089266e2352cb313d55ee18b849e39c418e8e9cd25dea8c4bf85fc4": { "6a3663dc0651e59d7ac6f549864cf1c084e3fbfc98ebd0479fed6a84075a378e": {
"displayName": "OAuthHandler/Code", "displayName": "OAuthHandler/Code",
"source": { "source": {
"path": "asset.99aabce70089266e2352cb313d55ee18b849e39c418e8e9cd25dea8c4bf85fc4", "path": "asset.6a3663dc0651e59d7ac6f549864cf1c084e3fbfc98ebd0479fed6a84075a378e",
"packaging": "zip" "packaging": "zip"
}, },
"destinations": { "destinations": {
"495395224548-us-east-1-793899ae": { "495395224548-us-east-1-fffb41e6": {
"bucketName": "cdk-hnb659fds-assets-495395224548-us-east-1", "bucketName": "cdk-hnb659fds-assets-495395224548-us-east-1",
"objectKey": "99aabce70089266e2352cb313d55ee18b849e39c418e8e9cd25dea8c4bf85fc4.zip", "objectKey": "6a3663dc0651e59d7ac6f549864cf1c084e3fbfc98ebd0479fed6a84075a378e.zip",
"region": "us-east-1", "region": "us-east-1",
"assumeRoleArn": "arn:${AWS::Partition}:iam::495395224548:role/cdk-hnb659fds-file-publishing-role-495395224548-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", "displayName": "Scheduler/Code",
"source": { "source": {
"path": "asset.8e7324457a5952eb51f04a34fbc5ba853252e7157d8d8958ac5fda92e72edb1f", "path": "asset.1aaec5c60ed6a294ff9b918a707477e0a0e299a850447e7456a8c8091604131b",
"packaging": "zip" "packaging": "zip"
}, },
"destinations": { "destinations": {
"495395224548-us-east-1-89bca2fb": { "495395224548-us-east-1-e6bab83a": {
"bucketName": "cdk-hnb659fds-assets-495395224548-us-east-1", "bucketName": "cdk-hnb659fds-assets-495395224548-us-east-1",
"objectKey": "8e7324457a5952eb51f04a34fbc5ba853252e7157d8d8958ac5fda92e72edb1f.zip", "objectKey": "1aaec5c60ed6a294ff9b918a707477e0a0e299a850447e7456a8c8091604131b.zip",
"region": "us-east-1", "region": "us-east-1",
"assumeRoleArn": "arn:${AWS::Partition}:iam::495395224548:role/cdk-hnb659fds-file-publishing-role-495395224548-us-east-1" "assumeRoleArn": "arn:${AWS::Partition}:iam::495395224548:role/cdk-hnb659fds-file-publishing-role-495395224548-us-east-1"
} }
} }
}, },
"78c322849179468ec994f5c3e550a0db4961592ea962b9cf484463e5f04b5a70": { "9c45c012ea9c045aa771b1c3049eadcb15fe66ca16d02e617d50ee9745fa967a": {
"displayName": "AgentClawStack Template", "displayName": "AgentClawStack Template",
"source": { "source": {
"path": "AgentClawStack.template.json", "path": "AgentClawStack.template.json",
"packaging": "file" "packaging": "file"
}, },
"destinations": { "destinations": {
"495395224548-us-east-1-02b0125a": { "495395224548-us-east-1-0ef056b9": {
"bucketName": "cdk-hnb659fds-assets-495395224548-us-east-1", "bucketName": "cdk-hnb659fds-assets-495395224548-us-east-1",
"objectKey": "78c322849179468ec994f5c3e550a0db4961592ea962b9cf484463e5f04b5a70.json", "objectKey": "9c45c012ea9c045aa771b1c3049eadcb15fe66ca16d02e617d50ee9745fa967a.json",
"region": "us-east-1", "region": "us-east-1",
"assumeRoleArn": "arn:${AWS::Partition}:iam::495395224548:role/cdk-hnb659fds-file-publishing-role-495395224548-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": [ "/AgentClawStack/SessionStore": [
{ {
"type": "aws:cdk:hasPhysicalName", "type": "aws:cdk:hasPhysicalName",
@@ -42,7 +32,7 @@
{ {
"type": "aws:cdk:creationStack", "type": "aws:cdk:creationStack",
"data": [ "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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...node internals, ts-node, ts-node, ts-node..."
] ]
@@ -56,7 +46,7 @@
{ {
"type": "aws:cdk:creationStack", "type": "aws:cdk:creationStack",
"data": [ "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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...node internals, ts-node, ts-node, ts-node..."
] ]
@@ -70,7 +60,7 @@
{ {
"type": "aws:cdk:creationStack", "type": "aws:cdk:creationStack",
"data": [ "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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...node internals, ts-node, ts-node, ts-node..."
] ]
@@ -84,7 +74,7 @@
{ {
"type": "aws:cdk:creationStack", "type": "aws:cdk:creationStack",
"data": [ "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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...node internals, ts-node, ts-node, ts-node..."
] ]
@@ -98,7 +88,7 @@
{ {
"type": "aws:cdk:creationStack", "type": "aws:cdk:creationStack",
"data": [ "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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...node internals, ts-node, ts-node, ts-node..."
] ]
@@ -112,7 +102,7 @@
{ {
"type": "aws:cdk:creationStack", "type": "aws:cdk:creationStack",
"data": [ "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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...node internals, ts-node, ts-node, ts-node..."
] ]
@@ -126,7 +116,7 @@
{ {
"type": "aws:cdk:creationStack", "type": "aws:cdk:creationStack",
"data": [ "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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...node internals, ts-node, ts-node, ts-node..."
] ]
@@ -140,7 +130,7 @@
{ {
"type": "aws:cdk:creationStack", "type": "aws:cdk:creationStack",
"data": [ "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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...node internals, ts-node, ts-node, ts-node..."
] ]
@@ -154,7 +144,7 @@
{ {
"type": "aws:cdk:creationStack", "type": "aws:cdk:creationStack",
"data": [ "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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...node internals, ts-node, ts-node, ts-node..."
] ]
@@ -168,7 +158,7 @@
{ {
"type": "aws:cdk:creationStack", "type": "aws:cdk:creationStack",
"data": [ "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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...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": [ "/AgentClawStack/SessionStore/Resource": [
{ {
"type": "aws:cdk:logicalId", "type": "aws:cdk:logicalId",
@@ -239,7 +199,7 @@
"type": "aws:cdk:creationStack", "type": "aws:cdk:creationStack",
"data": [ "data": [
"...new Table2 in aws-cdk-lib...", "...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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...node internals, ts-node, ts-node, ts-node..."
] ]
@@ -254,7 +214,7 @@
"type": "aws:cdk:creationStack", "type": "aws:cdk:creationStack",
"data": [ "data": [
"...new Table2 in aws-cdk-lib...", "...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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...node internals, ts-node, ts-node, ts-node..."
] ]
@@ -269,7 +229,7 @@
"type": "aws:cdk:creationStack", "type": "aws:cdk:creationStack",
"data": [ "data": [
"...new Queue2 in aws-cdk-lib...", "...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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...node internals, ts-node, ts-node, ts-node..."
] ]
@@ -284,7 +244,7 @@
"type": "aws:cdk:creationStack", "type": "aws:cdk:creationStack",
"data": [ "data": [
"...new Function2 in aws-cdk-lib...", "...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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...node internals, ts-node, ts-node, ts-node..."
] ]
@@ -299,7 +259,7 @@
"type": "aws:cdk:creationStack", "type": "aws:cdk:creationStack",
"data": [ "data": [
"...new Function2 in aws-cdk-lib...", "...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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...node internals, ts-node, ts-node, ts-node..."
] ]
@@ -314,7 +274,7 @@
"type": "aws:cdk:creationStack", "type": "aws:cdk:creationStack",
"data": [ "data": [
"...new HttpApi2 in aws-cdk-lib...", "...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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...node internals, ts-node, ts-node, ts-node..."
] ]
@@ -329,7 +289,7 @@
"type": "aws:cdk:creationStack", "type": "aws:cdk:creationStack",
"data": [ "data": [
"...new Role2 in aws-cdk-lib...", "...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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...node internals, ts-node, ts-node, ts-node..."
] ]
@@ -344,7 +304,7 @@
"type": "aws:cdk:creationStack", "type": "aws:cdk:creationStack",
"data": [ "data": [
"...new Function2 in aws-cdk-lib...", "...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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...node internals, ts-node, ts-node, ts-node..."
] ]
@@ -359,7 +319,7 @@
"type": "aws:cdk:creationStack", "type": "aws:cdk:creationStack",
"data": [ "data": [
"...new Function2 in aws-cdk-lib...", "...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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...node internals, ts-node, ts-node, ts-node..."
] ]
@@ -374,7 +334,7 @@
"type": "aws:cdk:creationStack", "type": "aws:cdk:creationStack",
"data": [ "data": [
"...new Rule2 in aws-cdk-lib...", "...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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...node internals, ts-node, ts-node, ts-node..."
] ]
@@ -389,7 +349,7 @@
"type": "aws:cdk:creationStack", "type": "aws:cdk:creationStack",
"data": [ "data": [
"...WrappedClass.<anonymous> in aws-cdk-lib...", "...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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...node internals, ts-node, ts-node, ts-node..."
] ]
@@ -404,7 +364,7 @@
"type": "aws:cdk:creationStack", "type": "aws:cdk:creationStack",
"data": [ "data": [
"...new Function2 in aws-cdk-lib...", "...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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...node internals, ts-node, ts-node, ts-node..."
] ]
@@ -419,7 +379,7 @@
"type": "aws:cdk:creationStack", "type": "aws:cdk:creationStack",
"data": [ "data": [
"...WrappedClass.addPermission in aws-cdk-lib...", "...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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...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": [ "/AgentClawStack/TgIngest/ServiceRole/Resource": [
{ {
"type": "aws:cdk:logicalId", "type": "aws:cdk:logicalId",
@@ -492,7 +407,7 @@
"type": "aws:cdk:creationStack", "type": "aws:cdk:creationStack",
"data": [ "data": [
"...new Function2 in aws-cdk-lib...", "...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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...node internals, ts-node, ts-node, ts-node..."
] ]
@@ -507,7 +422,7 @@
"type": "aws:cdk:creationStack", "type": "aws:cdk:creationStack",
"data": [ "data": [
"...new Function2 in aws-cdk-lib...", "...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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...node internals, ts-node, ts-node, ts-node..."
] ]
@@ -522,7 +437,7 @@
"type": "aws:cdk:creationStack", "type": "aws:cdk:creationStack",
"data": [ "data": [
"...WrappedClass.addEventSource in aws-cdk-lib...", "...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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...node internals, ts-node, ts-node, ts-node..."
] ]
@@ -537,7 +452,7 @@
"type": "aws:cdk:creationStack", "type": "aws:cdk:creationStack",
"data": [ "data": [
"...new HttpApi2 in aws-cdk-lib...", "...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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...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...", ".../Users/daniel/agent-claw/cdk/node_modules/aws-cdk-lib/aws-apigatewayv2/lib/http/api.js:1:96 in aws-cdk-lib...",
"Array.map (:)", "Array.map (:)",
"...WrappedClass.<anonymous> in aws-cdk-lib...", "...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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...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...", ".../Users/daniel/agent-claw/cdk/node_modules/aws-cdk-lib/aws-apigatewayv2/lib/http/api.js:1:96 in aws-cdk-lib...",
"Array.map (:)", "Array.map (:)",
"...WrappedClass.<anonymous> in aws-cdk-lib...", "...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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...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...", ".../Users/daniel/agent-claw/cdk/node_modules/aws-cdk-lib/aws-apigatewayv2/lib/http/api.js:1:96 in aws-cdk-lib...",
"Array.map (:)", "Array.map (:)",
"...WrappedClass.<anonymous> in aws-cdk-lib...", "...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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...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...", ".../Users/daniel/agent-claw/cdk/node_modules/aws-cdk-lib/aws-apigatewayv2/lib/http/api.js:1:96 in aws-cdk-lib...",
"Array.map (:)", "Array.map (:)",
"...WrappedClass.<anonymous> in aws-cdk-lib...", "...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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...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...", ".../Users/daniel/agent-claw/cdk/node_modules/aws-cdk-lib/aws-apigatewayv2/lib/http/api.js:1:96 in aws-cdk-lib...",
"Array.map (:)", "Array.map (:)",
"...WrappedClass.<anonymous> in aws-cdk-lib...", "...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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...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...", ".../Users/daniel/agent-claw/cdk/node_modules/aws-cdk-lib/aws-apigatewayv2/lib/http/api.js:1:96 in aws-cdk-lib...",
"Array.map (:)", "Array.map (:)",
"...WrappedClass.<anonymous> in aws-cdk-lib...", "...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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...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...", ".../Users/daniel/agent-claw/cdk/node_modules/aws-cdk-lib/aws-apigatewayv2/lib/http/api.js:1:96 in aws-cdk-lib...",
"Array.map (:)", "Array.map (:)",
"...WrappedClass.<anonymous> in aws-cdk-lib...", "...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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...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...", ".../Users/daniel/agent-claw/cdk/node_modules/aws-cdk-lib/aws-apigatewayv2/lib/http/api.js:1:96 in aws-cdk-lib...",
"Array.map (:)", "Array.map (:)",
"...WrappedClass.<anonymous> in aws-cdk-lib...", "...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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...node internals, ts-node, ts-node, ts-node..."
] ]
@@ -688,7 +603,7 @@
"type": "aws:cdk:creationStack", "type": "aws:cdk:creationStack",
"data": [ "data": [
"...WrappedClass.<anonymous> in aws-cdk-lib...", "...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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...node internals, ts-node, ts-node, ts-node..."
] ]
@@ -702,8 +617,8 @@
{ {
"type": "aws:cdk:creationStack", "type": "aws:cdk:creationStack",
"data": [ "data": [
"...environmentFromArn.grantRead in aws-cdk-lib...", "...WrappedClass.<anonymous> in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:201:29)", "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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...node internals, ts-node, ts-node, ts-node..."
] ]
@@ -718,7 +633,7 @@
"type": "aws:cdk:creationStack", "type": "aws:cdk:creationStack",
"data": [ "data": [
"...new Function2 in aws-cdk-lib...", "...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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...node internals, ts-node, ts-node, ts-node..."
] ]
@@ -733,7 +648,7 @@
"type": "aws:cdk:creationStack", "type": "aws:cdk:creationStack",
"data": [ "data": [
"...new Function2 in aws-cdk-lib...", "...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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...node internals, ts-node, ts-node, ts-node..."
] ]
@@ -748,24 +663,7 @@
"type": "aws:cdk:creationStack", "type": "aws:cdk:creationStack",
"data": [ "data": [
"...new Function2 in aws-cdk-lib...", "...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..."
]
}
],
"/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)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...node internals, ts-node, ts-node, ts-node..."
] ]
@@ -780,7 +678,7 @@
"type": "aws:cdk:creationStack", "type": "aws:cdk:creationStack",
"data": [ "data": [
"...WrappedClass.grantSendMessages in aws-cdk-lib...", "...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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...node internals, ts-node, ts-node, ts-node..."
] ]
@@ -795,7 +693,7 @@
"type": "aws:cdk:creationStack", "type": "aws:cdk:creationStack",
"data": [ "data": [
"...WrappedClass.grantReadWriteData in aws-cdk-lib...", "...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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...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...", ".../Users/daniel/agent-claw/cdk/node_modules/aws-cdk-lib/aws-apigatewayv2/lib/http/api.js:1:96 in aws-cdk-lib...",
"Array.map (:)", "Array.map (:)",
"...WrappedClass.<anonymous> in aws-cdk-lib...", "...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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...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...", ".../Users/daniel/agent-claw/cdk/node_modules/aws-cdk-lib/aws-apigatewayv2/lib/http/api.js:1:96 in aws-cdk-lib...",
"Array.map (:)", "Array.map (:)",
"...WrappedClass.<anonymous> in aws-cdk-lib...", "...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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...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...", ".../Users/daniel/agent-claw/cdk/node_modules/aws-cdk-lib/aws-apigatewayv2/lib/http/api.js:1:96 in aws-cdk-lib...",
"Array.map (:)", "Array.map (:)",
"...WrappedClass.<anonymous> in aws-cdk-lib...", "...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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...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...", ".../Users/daniel/agent-claw/cdk/node_modules/aws-cdk-lib/aws-apigatewayv2/lib/http/api.js:1:96 in aws-cdk-lib...",
"Array.map (:)", "Array.map (:)",
"...WrappedClass.<anonymous> in aws-cdk-lib...", "...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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...node internals, ts-node, ts-node, ts-node..."
] ]
@@ -877,8 +775,8 @@
{ {
"type": "aws:cdk:creationStack", "type": "aws:cdk:creationStack",
"data": [ "data": [
"...environmentFromArn.grantRead in aws-cdk-lib...", "...WrappedClass.addToRolePolicy in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:258:29)", "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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...node internals, ts-node, ts-node, ts-node..."
] ]
@@ -893,7 +791,7 @@
"type": "aws:cdk:creationStack", "type": "aws:cdk:creationStack",
"data": [ "data": [
"...WrappedClass.grantSendMessages in aws-cdk-lib...", "...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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...node internals, ts-node, ts-node, ts-node..."
] ]
@@ -907,8 +805,8 @@
{ {
"type": "aws:cdk:creationStack", "type": "aws:cdk:creationStack",
"data": [ "data": [
"...environmentFromArn.grantRead in aws-cdk-lib...", "...WrappedClass.addToRolePolicy in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:348:20)", "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)", "<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..." "...node internals, ts-node, ts-node, ts-node..."
] ]

View File

@@ -1,238 +1,6 @@
{ {
"Description": "agent-claw: serverless personal assistant on AgentCore", "Description": "agent-claw: serverless personal assistant on AgentCore",
"Resources": { "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": { "SessionStore8C86EEFE": {
"Type": "AWS::DynamoDB::Table", "Type": "AWS::DynamoDB::Table",
"Properties": { "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": [ "Action": [
"secretsmanager:GetSecretValue", "s3:DeleteObject*",
"secretsmanager:DescribeSecret" "s3:PutObject",
"s3:PutObjectLegalHold",
"s3:PutObjectRetention",
"s3:PutObjectTagging",
"s3:PutObjectVersionTagging",
"s3:Abort*"
], ],
"Effect": "Allow", "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" "Version": "2012-10-17"
@@ -380,15 +187,16 @@
"Properties": { "Properties": {
"Code": { "Code": {
"S3Bucket": "cdk-hnb659fds-assets-495395224548-us-east-1", "S3Bucket": "cdk-hnb659fds-assets-495395224548-us-east-1",
"S3Key": "f4461651bfa7d2822e3f36525ace7882e1610dcdaf85e052e1907241e25491d6.zip" "S3Key": "e0a834c0c682fe0c528540082d0b587c43ed791927a064104e5eb6c507c0cdb2.zip"
}, },
"Environment": { "Environment": {
"Variables": { "Variables": {
"MESSAGE_QUEUE_URL": { "MESSAGE_QUEUE_URL": {
"Ref": "MessageQueue7A3BF959" "Ref": "MessageQueue7A3BF959"
}, },
"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",
"TELEGRAM_WEBHOOK_SECRET": "" "TELEGRAM_WEBHOOK_SECRET": "",
"ATTACHMENTS_BUCKET_NAME": "agent-claw-workspace-495395224548"
} }
}, },
"FunctionName": "agent-claw-tg-ingest", "FunctionName": "agent-claw-tg-ingest",
@@ -409,7 +217,7 @@
], ],
"Metadata": { "Metadata": {
"aws:cdk:path": "AgentClawStack/TgIngest/Resource", "aws:cdk:path": "AgentClawStack/TgIngest/Resource",
"aws:asset:path": "asset.f4461651bfa7d2822e3f36525ace7882e1610dcdaf85e052e1907241e25491d6", "aws:asset:path": "asset.e0a834c0c682fe0c528540082d0b587c43ed791927a064104e5eb6c507c0cdb2",
"aws:asset:is-bundled": false, "aws:asset:is-bundled": false,
"aws:asset:property": "Code" "aws:asset:property": "Code"
} }
@@ -538,42 +346,39 @@
"Effect": "Allow", "Effect": "Allow",
"Resource": [ "Resource": [
{ {
"Fn::GetAtt": [ "Fn::Join": [
"WorkspaceBucket53E30B92", "",
"Arn" [
"arn:",
{
"Ref": "AWS::Partition"
},
":s3:::agent-claw-workspace-495395224548"
]
] ]
}, },
{ {
"Fn::Join": [ "Fn::Join": [
"", "",
[ [
"arn:",
{ {
"Fn::GetAtt": [ "Ref": "AWS::Partition"
"WorkspaceBucket53E30B92",
"Arn"
]
}, },
"/*" ":s3:::agent-claw-workspace-495395224548/*"
] ]
] ]
} }
] ]
}, },
{ {
"Action": [ "Action": "ssm:GetParameter",
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
],
"Effect": "Allow", "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",
"Action": [ "arn:aws:ssm:us-east-1:495395224548:parameter/agent-claw/google-oauth-client"
"secretsmanager:GetSecretValue", ]
"secretsmanager:DescribeSecret"
],
"Effect": "Allow",
"Resource": "arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/brave-api-key-uUSgzi"
}, },
{ {
"Action": [ "Action": [
@@ -615,18 +420,16 @@
"Properties": { "Properties": {
"Code": { "Code": {
"S3Bucket": "cdk-hnb659fds-assets-495395224548-us-east-1", "S3Bucket": "cdk-hnb659fds-assets-495395224548-us-east-1",
"S3Key": "e8d92532d2cb081ba122764c803acc80aaa41350d3497665468ca165dd5ff799.zip" "S3Key": "59b4a808a6125020261c01ae23500d036cdde29fdb255aa6a82d61555fde4848.zip"
}, },
"Environment": { "Environment": {
"Variables": { "Variables": {
"SESSION_TABLE_NAME": { "SESSION_TABLE_NAME": {
"Ref": "SessionStore8C86EEFE" "Ref": "SessionStore8C86EEFE"
}, },
"WORKSPACE_BUCKET_NAME": { "WORKSPACE_BUCKET_NAME": "agent-claw-workspace-495395224548",
"Ref": "WorkspaceBucket53E30B92" "TELEGRAM_BOT_TOKEN_SSM_PARAM": "/agent-claw/telegram-bot-token",
}, "BRAVE_API_KEY_SSM_PARAM": "/agent-claw/brave-api-key",
"TELEGRAM_BOT_TOKEN_SECRET_ARN": "arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/telegram-bot-token-Oq3in3",
"BRAVE_API_KEY_SECRET_ARN": "arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/brave-api-key-uUSgzi",
"RUNTIME_1_ARN": "arn:aws:bedrock-agentcore:us-east-1:495395224548:runtime/agentclaw_agent_claw_main-vTRGIEG6ON", "RUNTIME_1_ARN": "arn:aws:bedrock-agentcore:us-east-1:495395224548:runtime/agentclaw_agent_claw_main-vTRGIEG6ON",
"AWS_REGION_NAME": "us-east-1", "AWS_REGION_NAME": "us-east-1",
"USERS_TABLE_NAME": { "USERS_TABLE_NAME": {
@@ -653,7 +456,7 @@
], ],
"Metadata": { "Metadata": {
"aws:cdk:path": "AgentClawStack/AgentRunner/Resource", "aws:cdk:path": "AgentClawStack/AgentRunner/Resource",
"aws:asset:path": "asset.e8d92532d2cb081ba122764c803acc80aaa41350d3497665468ca165dd5ff799", "aws:asset:path": "asset.59b4a808a6125020261c01ae23500d036cdde29fdb255aa6a82d61555fde4848",
"aws:asset:is-bundled": false, "aws:asset:is-bundled": false,
"aws:asset:property": "Code" "aws:asset:property": "Code"
} }
@@ -1055,42 +858,39 @@
"Effect": "Allow", "Effect": "Allow",
"Resource": [ "Resource": [
{ {
"Fn::GetAtt": [ "Fn::Join": [
"WorkspaceBucket53E30B92", "",
"Arn" [
"arn:",
{
"Ref": "AWS::Partition"
},
":s3:::agent-claw-workspace-495395224548"
]
] ]
}, },
{ {
"Fn::Join": [ "Fn::Join": [
"", "",
[ [
"arn:",
{ {
"Fn::GetAtt": [ "Ref": "AWS::Partition"
"WorkspaceBucket53E30B92",
"Arn"
]
}, },
"/*" ":s3:::agent-claw-workspace-495395224548/*"
] ]
] ]
} }
] ]
}, },
{ {
"Action": [ "Action": "ssm:GetParameter",
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
],
"Effect": "Allow", "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",
"Action": [ "arn:aws:ssm:us-east-1:495395224548:parameter/agent-claw/google-oauth-client"
"secretsmanager:GetSecretValue", ]
"secretsmanager:DescribeSecret"
],
"Effect": "Allow",
"Resource": "arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/brave-api-key-uUSgzi"
}, },
{ {
"Action": [ "Action": [
@@ -1161,14 +961,6 @@
}, },
"Sid": "WorkspaceMcpInvoke" "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", "Action": "secretsmanager:GetSecretValue",
"Effect": "Allow", "Effect": "Allow",
@@ -1207,6 +999,72 @@
] ]
}, },
"Sid": "SchedulerLambdaPermission" "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" "Version": "2012-10-17"
@@ -1228,12 +1086,13 @@
"PolicyDocument": { "PolicyDocument": {
"Statement": [ "Statement": [
{ {
"Action": [ "Action": "ssm:GetParameter",
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
],
"Effect": "Allow", "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", "Action": "secretsmanager:GetSecretValue",
@@ -1293,20 +1152,13 @@
"PolicyDocument": { "PolicyDocument": {
"Statement": [ "Statement": [
{ {
"Action": [ "Action": "ssm:GetParameter",
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
],
"Effect": "Allow", "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",
"Action": [ "arn:aws:ssm:us-east-1:495395224548:parameter/agent-claw/google-oauth-client"
"secretsmanager:GetSecretValue", ]
"secretsmanager:DescribeSecret"
],
"Effect": "Allow",
"Resource": "arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/telegram-bot-token-Oq3in3"
}, },
{ {
"Action": [ "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": [ "Action": [
"secretsmanager:CreateSecret", "secretsmanager:CreateSecret",
@@ -1381,15 +1227,15 @@
"Properties": { "Properties": {
"Code": { "Code": {
"S3Bucket": "cdk-hnb659fds-assets-495395224548-us-east-1", "S3Bucket": "cdk-hnb659fds-assets-495395224548-us-east-1",
"S3Key": "99aabce70089266e2352cb313d55ee18b849e39c418e8e9cd25dea8c4bf85fc4.zip" "S3Key": "6a3663dc0651e59d7ac6f549864cf1c084e3fbfc98ebd0479fed6a84075a378e.zip"
}, },
"Environment": { "Environment": {
"Variables": { "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": { "USERS_TABLE_NAME": {
"Ref": "UsersTable9725E9C8" "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": { "OAUTH_REDIRECT_URI": {
"Fn::Join": [ "Fn::Join": [
"", "",
@@ -1426,7 +1272,7 @@
], ],
"Metadata": { "Metadata": {
"aws:cdk:path": "AgentClawStack/OAuthHandler/Resource", "aws:cdk:path": "AgentClawStack/OAuthHandler/Resource",
"aws:asset:path": "asset.99aabce70089266e2352cb313d55ee18b849e39c418e8e9cd25dea8c4bf85fc4", "aws:asset:path": "asset.6a3663dc0651e59d7ac6f549864cf1c084e3fbfc98ebd0479fed6a84075a378e",
"aws:asset:is-bundled": false, "aws:asset:is-bundled": false,
"aws:asset:property": "Code" "aws:asset:property": "Code"
} }
@@ -1656,12 +1502,13 @@
"PolicyDocument": { "PolicyDocument": {
"Statement": [ "Statement": [
{ {
"Action": [ "Action": "ssm:GetParameter",
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
],
"Effect": "Allow", "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": [ "Action": [
@@ -1690,11 +1537,11 @@
"Properties": { "Properties": {
"Code": { "Code": {
"S3Bucket": "cdk-hnb659fds-assets-495395224548-us-east-1", "S3Bucket": "cdk-hnb659fds-assets-495395224548-us-east-1",
"S3Key": "8e7324457a5952eb51f04a34fbc5ba853252e7157d8d8958ac5fda92e72edb1f.zip" "S3Key": "1aaec5c60ed6a294ff9b918a707477e0a0e299a850447e7456a8c8091604131b.zip"
}, },
"Environment": { "Environment": {
"Variables": { "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", "FunctionName": "agent-claw-scheduler",
@@ -1715,7 +1562,7 @@
], ],
"Metadata": { "Metadata": {
"aws:cdk:path": "AgentClawStack/Scheduler/Resource", "aws:cdk:path": "AgentClawStack/Scheduler/Resource",
"aws:asset:path": "asset.8e7324457a5952eb51f04a34fbc5ba853252e7157d8d8958ac5fda92e72edb1f", "aws:asset:path": "asset.1aaec5c60ed6a294ff9b918a707477e0a0e299a850447e7456a8c8091604131b",
"aws:asset:is-bundled": false, "aws:asset:is-bundled": false,
"aws:asset:property": "Code" "aws:asset:property": "Code"
} }
@@ -1740,7 +1587,7 @@
"CDKMetadata": { "CDKMetadata": {
"Type": "AWS::CDK::Metadata", "Type": "AWS::CDK::Metadata",
"Properties": { "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": { "Metadata": {
"aws:cdk:path": "AgentClawStack/CDKMetadata/Default" "aws:cdk:path": "AgentClawStack/CDKMetadata/Default"
@@ -1811,9 +1658,7 @@
}, },
"WorkspaceBucketName": { "WorkspaceBucketName": {
"Description": "S3 bucket containing agent workspace files", "Description": "S3 bucket containing agent workspace files",
"Value": { "Value": "agent-claw-workspace-495395224548"
"Ref": "WorkspaceBucket53E30B92"
}
}, },
"SessionTableName": { "SessionTableName": {
"Description": "DynamoDB table for session mapping", "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, "validateOnSynth": false,
"assumeRoleArn": "arn:${AWS::Partition}:iam::495395224548:role/cdk-hnb659fds-deploy-role-495395224548-us-east-1", "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", "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, "requiresBootstrapStackVersion": 6,
"bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version",
"additionalDependencies": [ "additionalDependencies": [

File diff suppressed because one or more lines are too long

View File

@@ -8,6 +8,7 @@ import * as apigatewayv2 from 'aws-cdk-lib/aws-apigatewayv2';
import * as apigatewayv2integrations from 'aws-cdk-lib/aws-apigatewayv2-integrations'; import * as apigatewayv2integrations from 'aws-cdk-lib/aws-apigatewayv2-integrations';
import * as iam from 'aws-cdk-lib/aws-iam'; import * as iam from 'aws-cdk-lib/aws-iam';
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager'; import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
import * as ssm from 'aws-cdk-lib/aws-ssm';
import * as events from 'aws-cdk-lib/aws-events'; import * as events from 'aws-cdk-lib/aws-events';
import * as eventsTargets from 'aws-cdk-lib/aws-events-targets'; import * as eventsTargets from 'aws-cdk-lib/aws-events-targets';
import { SqsEventSource } from 'aws-cdk-lib/aws-lambda-event-sources'; import { SqsEventSource } from 'aws-cdk-lib/aws-lambda-event-sources';
@@ -20,25 +21,33 @@ export class AgentClawStack extends cdk.Stack {
super(scope, id, props); super(scope, id, props);
// ── Context parameters ───────────────────────────────────────────────── // ── Context parameters ─────────────────────────────────────────────────
const telegramBotTokenSecretArn = this.node.tryGetContext('telegramBotTokenSecretArn') as string | undefined; const telegramBotTokenParamName = this.node.tryGetContext('telegramBotTokenParamName') as string | undefined;
const braveApiKeySecretArn = this.node.tryGetContext('braveApiKeySecretArn') as string | undefined; const braveApiKeyParamName = this.node.tryGetContext('braveApiKeyParamName') as string | undefined;
const googleOAuthClientParamName = this.node.tryGetContext('googleOAuthClientParamName') as string | undefined;
const existingWorkspaceBucketName = this.node.tryGetContext('workspaceBucketName') as string | undefined; const existingWorkspaceBucketName = this.node.tryGetContext('workspaceBucketName') as string | undefined;
const runtime1Arn = this.node.tryGetContext('runtime1Arn') as string | undefined; const runtime1Arn = this.node.tryGetContext('runtime1Arn') as string | undefined;
if (!telegramBotTokenSecretArn) { if (!telegramBotTokenParamName) {
throw new Error('Context param required: telegramBotTokenSecretArn'); throw new Error('Context param required: telegramBotTokenParamName');
} }
if (!braveApiKeySecretArn) { if (!braveApiKeyParamName) {
throw new Error('Context param required: braveApiKeySecretArn'); throw new Error('Context param required: braveApiKeyParamName');
}
if (!googleOAuthClientParamName) {
throw new Error('Context param required: googleOAuthClientParamName');
} }
// ── Secrets (reference existing) ─────────────────────────────────────── // ── SSM Parameters (reference existing SecureString params) ────────────
const botTokenSecret = secretsmanager.Secret.fromSecretCompleteArn( const ssmParamArns = [
this, 'TelegramBotToken', telegramBotTokenSecretArn `arn:aws:ssm:${this.region}:${this.account}:parameter${telegramBotTokenParamName}`,
); `arn:aws:ssm:${this.region}:${this.account}:parameter${braveApiKeyParamName}`,
const braveApiKeySecret = secretsmanager.Secret.fromSecretCompleteArn( `arn:aws:ssm:${this.region}:${this.account}:parameter${googleOAuthClientParamName}`,
this, 'BraveApiKey', braveApiKeySecretArn ];
);
const ssmReadPolicy = new iam.PolicyStatement({
actions: ['ssm:GetParameter'],
resources: ssmParamArns,
});
// ── S3 workspace bucket ──────────────────────────────────────────────── // ── S3 workspace bucket ────────────────────────────────────────────────
const workspaceBucket = existingWorkspaceBucketName const workspaceBucket = existingWorkspaceBucketName
@@ -94,12 +103,14 @@ export class AgentClawStack extends cdk.Stack {
memorySize: 128, memorySize: 128,
environment: { environment: {
MESSAGE_QUEUE_URL: messageQueue.queueUrl, MESSAGE_QUEUE_URL: messageQueue.queueUrl,
TELEGRAM_BOT_TOKEN_SECRET_ARN: telegramBotTokenSecretArn, TELEGRAM_BOT_TOKEN_SSM_PARAM: telegramBotTokenParamName,
TELEGRAM_WEBHOOK_SECRET: '', // set via SSM or direct env after deploy TELEGRAM_WEBHOOK_SECRET: '', // set via SSM or direct env after deploy
ATTACHMENTS_BUCKET_NAME: workspaceBucket.bucketName,
}, },
}); });
messageQueue.grantSendMessages(tgIngestFn); messageQueue.grantSendMessages(tgIngestFn);
botTokenSecret.grantRead(tgIngestFn); tgIngestFn.addToRolePolicy(ssmReadPolicy);
workspaceBucket.grantWrite(tgIngestFn);
// ── Lambda: agent-runner ─────────────────────────────────────────────── // ── Lambda: agent-runner ───────────────────────────────────────────────
const agentRunnerFn = new lambda.Function(this, 'AgentRunner', { const agentRunnerFn = new lambda.Function(this, 'AgentRunner', {
@@ -112,8 +123,8 @@ export class AgentClawStack extends cdk.Stack {
environment: { environment: {
SESSION_TABLE_NAME: sessionTable.tableName, SESSION_TABLE_NAME: sessionTable.tableName,
WORKSPACE_BUCKET_NAME: workspaceBucket.bucketName, WORKSPACE_BUCKET_NAME: workspaceBucket.bucketName,
TELEGRAM_BOT_TOKEN_SECRET_ARN: telegramBotTokenSecretArn, TELEGRAM_BOT_TOKEN_SSM_PARAM: telegramBotTokenParamName,
BRAVE_API_KEY_SECRET_ARN: braveApiKeySecretArn, BRAVE_API_KEY_SSM_PARAM: braveApiKeyParamName,
RUNTIME_1_ARN: runtime1Arn ?? 'PLACEHOLDER_SET_AFTER_RUNTIME_DEPLOY', RUNTIME_1_ARN: runtime1Arn ?? 'PLACEHOLDER_SET_AFTER_RUNTIME_DEPLOY',
AWS_REGION_NAME: 'us-east-1', AWS_REGION_NAME: 'us-east-1',
}, },
@@ -123,8 +134,7 @@ export class AgentClawStack extends cdk.Stack {
usersTable.grantReadWriteData(agentRunnerFn); usersTable.grantReadWriteData(agentRunnerFn);
agentRunnerFn.addEnvironment('USERS_TABLE_NAME', usersTable.tableName); agentRunnerFn.addEnvironment('USERS_TABLE_NAME', usersTable.tableName);
workspaceBucket.grantRead(agentRunnerFn); workspaceBucket.grantRead(agentRunnerFn);
botTokenSecret.grantRead(agentRunnerFn); agentRunnerFn.addToRolePolicy(ssmReadPolicy);
braveApiKeySecret.grantRead(agentRunnerFn);
messageQueue.grantConsumeMessages(agentRunnerFn); messageQueue.grantConsumeMessages(agentRunnerFn);
// AgentCore invoke permission // AgentCore invoke permission
@@ -170,8 +180,7 @@ export class AgentClawStack extends cdk.Stack {
resources: ['*'], resources: ['*'],
})); }));
workspaceBucket.grantRead(runtime1Role); workspaceBucket.grantRead(runtime1Role);
botTokenSecret.grantRead(runtime1Role); runtime1Role.addToPolicy(ssmReadPolicy);
braveApiKeySecret.grantRead(runtime1Role);
usersTable.grantReadWriteData(runtime1Role); usersTable.grantReadWriteData(runtime1Role);
// Google secret grants added after workspace_mcp section below // Google secret grants added after workspace_mcp section below
runtime1Role.addToPolicy(new iam.PolicyStatement({ runtime1Role.addToPolicy(new iam.PolicyStatement({
@@ -190,15 +199,12 @@ export class AgentClawStack extends cdk.Stack {
// and fed back as context param runtime1Arn. // and fed back as context param runtime1Arn.
// ── Google Workspace MCP ────────────────────────────────────────────── // ── Google Workspace MCP ──────────────────────────────────────────────
const googleOAuthClientSecret = secretsmanager.Secret.fromSecretCompleteArn(
this, 'GoogleOAuthClient', 'arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/google-oauth-client-subXHl'
);
// workspace-mcp Lambda execution role (import existing — created during initial setup) // workspace-mcp Lambda execution role (import existing — created during initial setup)
const _workspaceMcpRole = iam.Role.fromRoleName( const _workspaceMcpRole = iam.Role.fromRoleName(
this, 'WorkspaceMcpRole', 'agent-claw-workspace-mcp-role' this, 'WorkspaceMcpRole', 'agent-claw-workspace-mcp-role'
); );
googleOAuthClientSecret.grantRead(_workspaceMcpRole); _workspaceMcpRole.addToPrincipalPolicy?.(ssmReadPolicy);
// Grant workspace-mcp role read access to all per-user Google credential secrets // Grant workspace-mcp role read access to all per-user Google credential secrets
(_workspaceMcpRole as iam.Role).addToPrincipalPolicy?.(new iam.PolicyStatement({ (_workspaceMcpRole as iam.Role).addToPrincipalPolicy?.(new iam.PolicyStatement({
sid: 'PerUserGoogleCredentialsRead', sid: 'PerUserGoogleCredentialsRead',
@@ -223,8 +229,7 @@ export class AgentClawStack extends cdk.Stack {
conditions: { StringEquals: { 'lambda:FunctionUrlAuthType': 'AWS_IAM' } }, conditions: { StringEquals: { 'lambda:FunctionUrlAuthType': 'AWS_IAM' } },
})); }));
// Grant AgentCore execution role read access to Google OAuth client + per-user credentials // Grant AgentCore execution role read access to per-user Google credentials (stays in Secrets Manager)
googleOAuthClientSecret.grantRead(runtime1Role);
runtime1Role.addToPolicy(new iam.PolicyStatement({ runtime1Role.addToPolicy(new iam.PolicyStatement({
sid: 'PerUserGoogleCredentialsReadRuntime', sid: 'PerUserGoogleCredentialsReadRuntime',
actions: ['secretsmanager:GetSecretValue'], actions: ['secretsmanager:GetSecretValue'],
@@ -248,22 +253,15 @@ export class AgentClawStack extends cdk.Stack {
timeout: cdk.Duration.seconds(30), timeout: cdk.Duration.seconds(30),
memorySize: 128, memorySize: 128,
environment: { environment: {
GOOGLE_OAUTH_CLIENT_SECRET_ARN: googleOAuthClientSecret.secretArn, GOOGLE_OAUTH_CLIENT_SSM_PARAM: googleOAuthClientParamName,
USERS_TABLE_NAME: usersTable.tableName, USERS_TABLE_NAME: usersTable.tableName,
TELEGRAM_BOT_TOKEN_SECRET_ARN: telegramBotTokenSecretArn, TELEGRAM_BOT_TOKEN_SSM_PARAM: telegramBotTokenParamName,
// OAUTH_REDIRECT_URI set after API GW URL is known — injected via addEnvironment below // OAUTH_REDIRECT_URI set after API GW URL is known — injected via addEnvironment below
OAUTH_REDIRECT_URI: 'PLACEHOLDER', OAUTH_REDIRECT_URI: 'PLACEHOLDER',
}, },
}); });
googleOAuthClientSecret.grantRead(oauthHandlerFn); oauthHandlerFn.addToRolePolicy(ssmReadPolicy);
botTokenSecret.grantRead(oauthHandlerFn);
usersTable.grantReadWriteData(oauthHandlerFn); usersTable.grantReadWriteData(oauthHandlerFn);
// Explicit access to the OAuth client secret (fromSecretNameV2 wildcard may not resolve)
oauthHandlerFn.addToRolePolicy(new iam.PolicyStatement({
sid: 'GoogleOAuthClientSecretExact',
actions: ['secretsmanager:GetSecretValue'],
resources: ['arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/google-oauth-client-subXHl'],
}));
// Grant OAuth handler write access to per-user credential secrets // Grant OAuth handler write access to per-user credential secrets
oauthHandlerFn.addToRolePolicy(new iam.PolicyStatement({ oauthHandlerFn.addToRolePolicy(new iam.PolicyStatement({
sid: 'PerUserGoogleCredentialsWrite', sid: 'PerUserGoogleCredentialsWrite',
@@ -342,10 +340,10 @@ export class AgentClawStack extends cdk.Stack {
timeout: cdk.Duration.seconds(30), timeout: cdk.Duration.seconds(30),
memorySize: 128, memorySize: 128,
environment: { environment: {
TELEGRAM_BOT_TOKEN_SECRET_ARN: telegramBotTokenSecretArn, TELEGRAM_BOT_TOKEN_SSM_PARAM: telegramBotTokenParamName,
}, },
}); });
botTokenSecret.grantRead(schedulerFn); schedulerFn.addToRolePolicy(ssmReadPolicy);
// Allow EventBridge to invoke the scheduler Lambda // Allow EventBridge to invoke the scheduler Lambda
schedulerFn.addPermission('EventBridgeInvoke', { schedulerFn.addPermission('EventBridgeInvoke', {
principal: new iam.ServicePrincipal('events.amazonaws.com'), principal: new iam.ServicePrincipal('events.amazonaws.com'),
@@ -373,6 +371,51 @@ export class AgentClawStack extends cdk.Stack {
resources: [schedulerFn.functionArn], 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 ──────────────────────────────────────────────────────────── // ── Outputs ────────────────────────────────────────────────────────────
new cdk.CfnOutput(this, 'WorkspaceMcpFunctionUrl', { 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

@@ -142,7 +142,10 @@ def handler(event, context):
channel = first.get('channel', 'telegram') channel = first.get('channel', 'telegram')
chat_id = first.get('chat_id', '') chat_id = first.get('chat_id', '')
message_thread_id = first.get('message_thread_id') # int or None message_thread_id = first.get('message_thread_id') # int or None
actor_id = f"{channel}:{chat_id}" # 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 ───────────────────────────────────────────────────── # ── User registry ─────────────────────────────────────────────────────
from_info = first.get('messages', [{}])[0] from_info = first.get('messages', [{}])[0]
@@ -160,11 +163,11 @@ def handler(event, context):
user_profile['status'] = 'active' user_profile['status'] = 'active'
prompt = f"[System: User just registered with name '{name}'. Welcome them warmly and ask how you can help.]" prompt = f"[System: User just registered with name '{name}'. Welcome them warmly and ask how you can help.]"
else: else:
bot_token_secret_arn = os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', '') bot_token_secret_arn = os.environ.get('TELEGRAM_BOT_TOKEN_SSM_PARAM', '')
bot_token = '' bot_token = ''
if bot_token_secret_arn: if bot_token_secret_arn:
sm = boto3.client('secretsmanager', region_name='us-east-1') ssm = boto3.client('ssm', region_name='us-east-1')
bot_token = sm.get_secret_value(SecretId=bot_token_secret_arn)['SecretString'] 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) send_telegram_direct(chat_id, bot_token, "Hi! I don't recognize you yet. What's your name?", thread_id=message_thread_id)
return return
# ── Get or create AgentCore session ────────────────────────────────── # ── Get or create AgentCore session ──────────────────────────────────
@@ -181,6 +184,18 @@ def handler(event, context):
] ]
prompt = f"You have {len(records)} queued messages:\n" + "\n".join(lines) 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 ──────────────────────────── # ── Build payload for AgentCore Runtime 1 ────────────────────────────
payload: dict[str, Any] = { payload: dict[str, Any] = {
'prompt': prompt, 'prompt': prompt,
@@ -197,7 +212,7 @@ def handler(event, context):
'type': channel, 'type': channel,
'target_id': str(chat_id), 'target_id': str(chat_id),
'message_thread_id': message_thread_id, 'message_thread_id': message_thread_id,
'bot_token_secret_arn': os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', ''), 'bot_token_secret_arn': os.environ.get('TELEGRAM_BOT_TOKEN_SSM_PARAM', ''),
}, },
} }
@@ -217,11 +232,11 @@ def handler(event, context):
# Process streaming response: buffer text chunks and send to Telegram as paragraphs arrive # Process streaming response: buffer text chunks and send to Telegram as paragraphs arrive
bot_token = '' bot_token = ''
bot_token_secret_arn = os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', '') bot_token_param = os.environ.get('TELEGRAM_BOT_TOKEN_SSM_PARAM', '')
if bot_token_secret_arn: if bot_token_param:
sm = boto3.client('secretsmanager', region_name='us-east-1') ssm = boto3.client('ssm', region_name='us-east-1')
try: try:
bot_token = sm.get_secret_value(SecretId=bot_token_secret_arn)['SecretString'] bot_token = ssm.get_parameter(Name=bot_token_param, WithDecryption=True)['Parameter']['Value']
except Exception as e: except Exception as e:
print(f'[agent-runner] Failed to get bot token: {e}') print(f'[agent-runner] Failed to get bot token: {e}')

View File

@@ -44,9 +44,10 @@ def get_ddb():
def get_oauth_client() -> tuple[str, str]: def get_oauth_client() -> tuple[str, str]:
"""Return (client_id, client_secret) from Secrets Manager.""" """Return (client_id, client_secret) from SSM Parameter Store."""
arn = os.environ['GOOGLE_OAUTH_CLIENT_SECRET_ARN'] param_name = os.environ['GOOGLE_OAUTH_CLIENT_SSM_PARAM']
secret = json.loads(get_sm().get_secret_value(SecretId=arn)['SecretString']) 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'] return secret['client_id'], secret['client_secret']
@@ -222,10 +223,11 @@ def handle_callback(params: dict) -> dict:
# Best-effort Telegram confirmation # Best-effort Telegram confirmation
try: try:
bot_token_arn = os.environ.get('TELEGRAM_BOT_TOKEN_SECRET_ARN', '') bot_token_param = os.environ.get('TELEGRAM_BOT_TOKEN_SSM_PARAM', '')
if bot_token_arn and actor_id.startswith('telegram:'): if bot_token_param and actor_id.startswith('telegram:'):
chat_id = actor_id.split(':', 1)[1] chat_id = actor_id.split(':', 1)[1]
bot_token = get_sm().get_secret_value(SecretId=bot_token_arn)['SecretString'] 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_text = f'✅ Connected {user_email} as "{label}"'
tg_payload = json.dumps({'chat_id': chat_id, 'text': tg_text}).encode() tg_payload = json.dumps({'chat_id': chat_id, 'text': tg_text}).encode()
tg_req = urllib.request.Request( tg_req = urllib.request.Request(

View File

@@ -11,8 +11,8 @@ def handler(event, context):
rule_name = event['rule_name'] rule_name = event['rule_name']
# Fetch bot token # Fetch bot token
sm = boto3.client('secretsmanager', region_name='us-east-1') ssm = boto3.client('ssm', region_name='us-east-1')
token = sm.get_secret_value(SecretId=os.environ['TELEGRAM_BOT_TOKEN_SECRET_ARN'])['SecretString'] token = ssm.get_parameter(Name=os.environ['TELEGRAM_BOT_TOKEN_SSM_PARAM'], WithDecryption=True)['Parameter']['Value']
# Send Telegram message # Send Telegram message
payload = json.dumps({'chat_id': chat_id, 'text': message}).encode() payload = json.dumps({'chat_id': chat_id, 'text': message}).encode()

View File

@@ -9,16 +9,22 @@ import boto3
_bot_token: str | None = None _bot_token: str | None = None
_token_lock = threading.Lock() _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: def get_bot_token() -> str:
global _bot_token global _bot_token
if _bot_token is None: if _bot_token is None:
with _token_lock: with _token_lock:
if _bot_token is None: if _bot_token is None:
sm = boto3.client('secretsmanager') ssm = boto3.client('ssm')
_bot_token = sm.get_secret_value( _bot_token = ssm.get_parameter(
SecretId=os.environ['TELEGRAM_BOT_TOKEN_SECRET_ARN'] Name=os.environ['TELEGRAM_BOT_TOKEN_SSM_PARAM'],
)['SecretString'] WithDecryption=True
)['Parameter']['Value']
return _bot_token return _bot_token
@@ -40,6 +46,64 @@ def send_typing(chat_id: str, thread_id: int | None = None) -> None:
pass # typing is best-effort 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): def handler(event, context):
# ── Validate Telegram webhook secret ────────────────────────────────── # ── Validate Telegram webhook secret ──────────────────────────────────
@@ -68,14 +132,69 @@ def handler(event, context):
chat_id = str(message.get('chat', {}).get('id', '')) chat_id = str(message.get('chat', {}).get('id', ''))
message_thread_id = message.get('message_thread_id') # present for supergroup topics message_thread_id = message.get('message_thread_id') # present for supergroup topics
text = message.get('text', '') text = message.get('text', '') or message.get('caption', '')
from_user = message.get('from', {}) from_user = message.get('from', {})
timestamp = message.get('date', 0) timestamp = message.get('date', 0)
print(f'[tg-ingest] chat_id={chat_id} text_len={len(text)} update_id={update_id}') # ── Detect file attachment ────────────────────────────────────────────
attachment = extract_attachment(message)
attachment_meta = None
if not chat_id or not text: if attachment:
print(f'[tg-ingest] Dropping: chat_id={chat_id!r} text={text!r}') 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'} return {'statusCode': 200, 'body': 'ok'}
# ── Send typing action (non-blocking, background thread) ────────────── # ── Send typing action (non-blocking, background thread) ──────────────
@@ -85,14 +204,10 @@ def handler(event, context):
# ── Enqueue to SQS FIFO ─────────────────────────────────────────────── # ── Enqueue to SQS FIFO ───────────────────────────────────────────────
sqs = boto3.client('sqs') sqs = boto3.client('sqs')
sqs.send_message( msg_body: dict = {
QueueUrl=os.environ['MESSAGE_QUEUE_URL'],
MessageGroupId=chat_id,
MessageDeduplicationId=str(update_id),
MessageBody=json.dumps({
'channel': 'telegram', 'channel': 'telegram',
'chat_id': chat_id, 'chat_id': chat_id,
'message_thread_id': message_thread_id, # None for regular chats, int for topics 'message_thread_id': message_thread_id,
'messages': [{ 'messages': [{
'text': text, 'text': text,
'from_id': str(from_user.get('id', '')), 'from_id': str(from_user.get('id', '')),
@@ -101,7 +216,15 @@ def handler(event, context):
}], }],
'update_id': update_id, 'update_id': update_id,
'timestamp': timestamp, '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'} return {'statusCode': 200, 'body': 'ok'}

View File

@@ -1,5 +1,5 @@
""" """
Fetch Google OAuth credentials and client secrets from Secrets Manager. Fetch Google OAuth credentials from SSM (client secret) and Secrets Manager (per-user tokens).
Called by bootstrap before starting workspace-mcp. Called by bootstrap before starting workspace-mcp.
""" """
import json import json
@@ -12,10 +12,11 @@ def main():
sm = boto3.client('secretsmanager', region_name=region) sm = boto3.client('secretsmanager', region_name=region)
# Fetch OAuth client credentials (client_id + client_secret) # Fetch OAuth client credentials (client_id + client_secret)
client_secret_arn = os.environ.get('GOOGLE_OAUTH_CLIENT_SECRET_ARN') client_secret_param = os.environ.get('GOOGLE_OAUTH_CLIENT_SSM_PARAM')
if client_secret_arn: if client_secret_param:
try: try:
client_creds = json.loads(sm.get_secret_value(SecretId=client_secret_arn)['SecretString']) ssm = boto3.client('ssm', region_name=region)
client_creds = json.loads(ssm.get_parameter(Name=client_secret_param, WithDecryption=True)['Parameter']['Value'])
os.environ['GOOGLE_OAUTH_CLIENT_ID'] = client_creds['client_id'] os.environ['GOOGLE_OAUTH_CLIENT_ID'] = client_creds['client_id']
os.environ['GOOGLE_OAUTH_CLIENT_SECRET'] = client_creds['client_secret'] os.environ['GOOGLE_OAUTH_CLIENT_SECRET'] = client_creds['client_secret']
print('[fetch_credentials] OAuth client credentials loaded', file=sys.stderr) print('[fetch_credentials] OAuth client credentials loaded', file=sys.stderr)

View File

@@ -21,11 +21,11 @@ def _setup_shared_environment():
os.environ.setdefault('HOME', '/tmp') os.environ.setdefault('HOME', '/tmp')
os.environ.setdefault('GOOGLE_WORKSPACE_MCP_CREDENTIALS_DIR', '/tmp/workspace_mcp_credentials') os.environ.setdefault('GOOGLE_WORKSPACE_MCP_CREDENTIALS_DIR', '/tmp/workspace_mcp_credentials')
client_arn = os.environ.get('GOOGLE_OAUTH_CLIENT_SECRET_ARN', '') client_param = os.environ.get('GOOGLE_OAUTH_CLIENT_SSM_PARAM', '')
if client_arn: if client_param:
try: try:
sm = boto3.client('secretsmanager', region_name=os.environ.get('AWS_REGION', 'us-east-1')) ssm = boto3.client('ssm', region_name=os.environ.get('AWS_REGION', 'us-east-1'))
client_creds = json.loads(sm.get_secret_value(SecretId=client_arn)['SecretString']) client_creds = json.loads(ssm.get_parameter(Name=client_param, WithDecryption=True)['Parameter']['Value'])
os.environ['GOOGLE_OAUTH_CLIENT_ID'] = client_creds['client_id'] os.environ['GOOGLE_OAUTH_CLIENT_ID'] = client_creds['client_id']
os.environ['GOOGLE_OAUTH_CLIENT_SECRET'] = client_creds['client_secret'] os.environ['GOOGLE_OAUTH_CLIENT_SECRET'] = client_creds['client_secret']
except Exception as e: except Exception as e:

View File

@@ -4,7 +4,9 @@ agent-claw Runtime 1 — main assistant agent.
Entrypoint for AgentCore CodeZip deployment. Entrypoint for AgentCore CodeZip deployment.
""" """
import os import os
import boto3
from strands import Agent, tool from strands import Agent, tool
from strands.models import BedrockModel
from bedrock_agentcore.runtime import BedrockAgentCoreApp from bedrock_agentcore.runtime import BedrockAgentCoreApp
from channels.telegram import TelegramAdapter from channels.telegram import TelegramAdapter
@@ -15,6 +17,10 @@ from tools import messaging
app = BedrockAgentCoreApp() 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 ────────────────────────────────────────────────────── # ── Tool definitions ──────────────────────────────────────────────────────
@@ -77,6 +83,7 @@ def main(payload: dict, context) -> dict:
# Create and run Strands agent # Create and run Strands agent
agent = Agent( agent = Agent(
model=BedrockModel(model_id=_model_id),
system_prompt=system_prompt, system_prompt=system_prompt,
tools=[send_message, web_search, web_fetch, read_workspace_file, write_workspace_file], 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.