Compare commits

..

21 Commits

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

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ agent-claw Runtime 1 — main assistant agent.
Entrypoint for AgentCore CodeZip deployment. 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
@@ -11,6 +12,7 @@ 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 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
@@ -34,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):
@@ -61,13 +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 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 ──────────────────────────────────────────────────────
@@ -263,6 +321,237 @@ def manage_service(action: str, service: str, config: dict | None = None) -> str
return f'Unknown action: {action}. Use "enroll", "remove", or "list".' 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)
@@ -365,20 +654,14 @@ async def main(payload: dict, context):
if ltm_block: if ltm_block:
system_prompt = system_prompt + '\n\n---\n\n' + ltm_block system_prompt = system_prompt + '\n\n---\n\n' + ltm_block
# Inject current datetime so the model always has accurate time context 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.'
from datetime import datetime 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.'
from zoneinfo import ZoneInfo
_tz = ZoneInfo('America/Chicago')
_now = datetime.now(_tz)
_time_str = _now.strftime('%A, %B %d, %Y %I:%M %p %Z')
system_prompt = system_prompt + f'\n\nCurrent date/time: {_time_str}'
print(f'[main] System prompt time injection: {_time_str}')
# 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),
) )
@@ -387,12 +670,18 @@ async def main(payload: dict, context):
home_assistant, connect_google_account, list_google_accounts, remove_google_account, home_assistant, connect_google_account, list_google_accounts, remove_google_account,
manage_service, manage_mcp_connection, 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] 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 # Load user's dynamic MCP connections
mcp_connections = services.get('mcp_connections', []) mcp_connections = services.get('mcp_connections', [])
mcp_clients, _mcp_to_close = mcp_loader.load_mcp_tools(mcp_connections, actor_id) mcp_clients, _mcp_to_close = mcp_loader.load_mcp_tools(mcp_connections, actor_id)
all_tools = base_tools + mcp_clients
# Load subagents from SSM
ssm = boto3.client('ssm', region_name='us-east-1')
subagent_tools = _load_subagents(ssm)
all_tools = base_tools + _aws_mcp_tools + mcp_clients + subagent_tools
agent = Agent( agent = Agent(
model=model, model=model,
@@ -401,9 +690,42 @@ async def main(payload: dict, context):
tools=all_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

View File

@@ -45,12 +45,40 @@ def _get_oauth_token(conn: dict, actor_id: str) -> str:
return token 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: def _resolve_auth_headers(conn: dict, actor_id: str) -> dict:
"""Resolve auth headers for a connection.""" """Resolve auth headers for a connection."""
auth_type = conn.get('auth_type', 'none') auth_type = conn.get('auth_type', 'none')
if auth_type == 'oauth_client_credentials': if auth_type == 'oauth_client_credentials':
token = _get_oauth_token(conn, actor_id) token = _get_oauth_token(conn, actor_id)
return {'Authorization': f'Bearer {token}'} 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': elif auth_type == 'bearer':
token = _get_ssm_value(conn['token_ssm']) token = _get_ssm_value(conn['token_ssm'])
return {'Authorization': f'Bearer {token}'} return {'Authorization': f'Bearer {token}'}

View File

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

View File

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

View File

@@ -15,6 +15,7 @@ dependencies = [
"strands-agents-tools >= 0.5.0", "strands-agents-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

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

@@ -371,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

@@ -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.