multi-tenant Phase 1: user registry + per-user memory

- CDK: add agent-claw-users DynamoDB table (actor_id PK, RETAIN policy)
- CDK: grant agent-runner read/write on users table; add USERS_TABLE_NAME env
- CDK: fix cdk.json app field (was object, must be command string)
- CDK: add UsersTableName output
- agent-runner: get_or_create_user() auto-registers users on first contact
  (stores display_name, telegram_username, created_at, allowed)
- agent-runner: pass user_profile in AgentCore payload
- prompt_builder: split base prompt (cached) from per-user context (injected per-call)
  removes USER.md/MEMORY.md from shared load; user name/username injected dynamically
- main.py: extract user_profile from payload, build user_context string for prompt
This commit is contained in:
daniel
2026-05-06 20:36:22 -05:00
parent 732b00fb66
commit 893c110729
16 changed files with 726 additions and 501 deletions

View File

@@ -140,8 +140,16 @@ def main(payload: dict, context) -> dict:
region_name='us-east-1',
)
# Build system prompt (cached across warm invocations)
system_prompt = build_system_prompt()
# Build system prompt — base cached, user context injected per-invocation
user_profile = payload.get('user_profile', {})
user_context = ''
if user_profile:
name = user_profile.get('display_name', '')
username = user_profile.get('telegram_username', '')
user_context = f'Name: {name}'
if username:
user_context += f'\nTelegram username: @{username}'
system_prompt = build_system_prompt(user_context=user_context)
# Model: claude-sonnet-4-6 via cross-region inference
model = BedrockModel(

View File

@@ -1,28 +1,35 @@
import os
import boto3
# Cache: built once per warm session
_system_prompt: str | None = None
# Cache: built once per warm session (shared base only)
_base_prompt: str | None = None
def build_system_prompt() -> str:
"""Build system prompt from S3 workspace files (cached for warm session)."""
global _system_prompt
if _system_prompt is not None:
return _system_prompt
def build_system_prompt(user_context: str = '') -> str:
"""Build system prompt from S3 workspace files + optional per-user context."""
base = _get_base_prompt()
if user_context:
return base + f'\n\n---\n\n## User\n{user_context}'
return base
def _get_base_prompt() -> str:
global _base_prompt
if _base_prompt is not None:
return _base_prompt
bucket = os.environ.get('WORKSPACE_BUCKET_NAME', '') or 'agent-claw-workspace-495395224548'
print(f'[prompt_builder] Loading from bucket: {bucket!r}')
if not bucket:
print('[prompt_builder] WARNING: WORKSPACE_BUCKET_NAME not set!')
_system_prompt = 'You are a helpful personal assistant.'
return _system_prompt
_base_prompt = 'You are a helpful personal assistant.'
return _base_prompt
s3 = boto3.client('s3')
parts = []
for fname in ['SOUL.md', 'AGENTS.md', 'IDENTITY.md', 'USER.md', 'MEMORY.md', 'TOOLS.md']:
for fname in ['SOUL.md', 'AGENTS.md', 'IDENTITY.md', 'TOOLS.md']:
try:
obj = s3.get_object(Bucket=bucket, Key=fname)
content = obj['Body'].read().decode('utf-8')
@@ -33,12 +40,12 @@ def build_system_prompt() -> str:
parts.append('## Runtime\nRuntime: agent-claw | host=AgentCore | model=bedrock-claude-sonnet | channel=telegram\nCurrent date/time is provided by the system. Timezone: America/Chicago.')
_system_prompt = '\n\n---\n\n'.join(parts)
print(f'[prompt_builder] System prompt built: {len(_system_prompt)} chars')
return _system_prompt
_base_prompt = '\n\n---\n\n'.join(parts)
print(f'[prompt_builder] Base prompt built: {len(_base_prompt)} chars')
return _base_prompt
def invalidate_prompt() -> None:
"""Force rebuild of system prompt on next invocation (call after workspace write)."""
global _system_prompt
_system_prompt = None
global _base_prompt
_base_prompt = None

View File

@@ -1,7 +1,5 @@
{
"app": {
"outdir": "cdk.out"
},
"app": "npx ts-node --prefer-ts-exts bin/agent-claw.ts",
"context": {
"@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true,
"@aws-cdk/core:stackRelativeExports": true

View File

@@ -1,91 +1,46 @@
{
"version": "53.0.0",
"files": {
"e2659170a0721541efa761a8d5d04d5e36cbbf691c4b15a9053002b7c825055d": {
"displayName": "WorkspaceFiles/AwsCliLayer/Code",
"source": {
"path": "asset.e2659170a0721541efa761a8d5d04d5e36cbbf691c4b15a9053002b7c825055d.zip",
"packaging": "file"
},
"destinations": {
"495395224548-us-east-1-b19c5879": {
"bucketName": "cdk-hnb659fds-assets-495395224548-us-east-1",
"objectKey": "e2659170a0721541efa761a8d5d04d5e36cbbf691c4b15a9053002b7c825055d.zip",
"region": "us-east-1",
"assumeRoleArn": "arn:${AWS::Partition}:iam::495395224548:role/cdk-hnb659fds-file-publishing-role-495395224548-us-east-1"
}
}
},
"3423a042b818e31c1e34a19d6689ab2e5f9b70fcbe9e71df66f241b20a200bd9": {
"displayName": "Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/Code",
"source": {
"path": "asset.3423a042b818e31c1e34a19d6689ab2e5f9b70fcbe9e71df66f241b20a200bd9",
"packaging": "zip"
},
"destinations": {
"495395224548-us-east-1-12f29a1a": {
"bucketName": "cdk-hnb659fds-assets-495395224548-us-east-1",
"objectKey": "3423a042b818e31c1e34a19d6689ab2e5f9b70fcbe9e71df66f241b20a200bd9.zip",
"region": "us-east-1",
"assumeRoleArn": "arn:${AWS::Partition}:iam::495395224548:role/cdk-hnb659fds-file-publishing-role-495395224548-us-east-1"
}
}
},
"d5a4044422f3c0ab39b0d5bfa4e4ea2b1212f0d420a58b542fbc88917d7a676a": {
"displayName": "WorkspaceFiles/Asset1",
"source": {
"path": "asset.d5a4044422f3c0ab39b0d5bfa4e4ea2b1212f0d420a58b542fbc88917d7a676a",
"packaging": "zip"
},
"destinations": {
"495395224548-us-east-1-2f513a77": {
"bucketName": "cdk-hnb659fds-assets-495395224548-us-east-1",
"objectKey": "d5a4044422f3c0ab39b0d5bfa4e4ea2b1212f0d420a58b542fbc88917d7a676a.zip",
"region": "us-east-1",
"assumeRoleArn": "arn:${AWS::Partition}:iam::495395224548:role/cdk-hnb659fds-file-publishing-role-495395224548-us-east-1"
}
}
},
"9d7af346bbad17b4c228d09e33a602eedc03747fe1cec1c7c9b7c8723ce74e5d": {
"8da48fd743d1e2cb70d8d1935cee795b6f8cf02609db05e2b8f28449be9ef875": {
"displayName": "TgIngest/Code",
"source": {
"path": "asset.9d7af346bbad17b4c228d09e33a602eedc03747fe1cec1c7c9b7c8723ce74e5d",
"path": "asset.8da48fd743d1e2cb70d8d1935cee795b6f8cf02609db05e2b8f28449be9ef875",
"packaging": "zip"
},
"destinations": {
"495395224548-us-east-1-e75a9fd4": {
"495395224548-us-east-1-0e5cdb5b": {
"bucketName": "cdk-hnb659fds-assets-495395224548-us-east-1",
"objectKey": "9d7af346bbad17b4c228d09e33a602eedc03747fe1cec1c7c9b7c8723ce74e5d.zip",
"objectKey": "8da48fd743d1e2cb70d8d1935cee795b6f8cf02609db05e2b8f28449be9ef875.zip",
"region": "us-east-1",
"assumeRoleArn": "arn:${AWS::Partition}:iam::495395224548:role/cdk-hnb659fds-file-publishing-role-495395224548-us-east-1"
}
}
},
"eeef9ac2146cd644e1727e77104b58bed992e19379d5070de3a05714ff2dba48": {
"7053cd1618f5f520a7aac409588128f920d8fe76791c1dbcc65610454d1a5387": {
"displayName": "AgentRunner/Code",
"source": {
"path": "asset.eeef9ac2146cd644e1727e77104b58bed992e19379d5070de3a05714ff2dba48",
"path": "asset.7053cd1618f5f520a7aac409588128f920d8fe76791c1dbcc65610454d1a5387",
"packaging": "zip"
},
"destinations": {
"495395224548-us-east-1-4a4b19df": {
"495395224548-us-east-1-63ace858": {
"bucketName": "cdk-hnb659fds-assets-495395224548-us-east-1",
"objectKey": "eeef9ac2146cd644e1727e77104b58bed992e19379d5070de3a05714ff2dba48.zip",
"objectKey": "7053cd1618f5f520a7aac409588128f920d8fe76791c1dbcc65610454d1a5387.zip",
"region": "us-east-1",
"assumeRoleArn": "arn:${AWS::Partition}:iam::495395224548:role/cdk-hnb659fds-file-publishing-role-495395224548-us-east-1"
}
}
},
"d7e0fade0cb46eefc22ea1239ac2735f5c6d3cf3829571a1c221c37e986ed966": {
"2765094d543818b111d837ea62bad41260a47615c5b99bc608a58e99f24d5b85": {
"displayName": "AgentClawStack Template",
"source": {
"path": "AgentClawStack.template.json",
"packaging": "file"
},
"destinations": {
"495395224548-us-east-1-2306706a": {
"495395224548-us-east-1-b10aaf8d": {
"bucketName": "cdk-hnb659fds-assets-495395224548-us-east-1",
"objectKey": "d7e0fade0cb46eefc22ea1239ac2735f5c6d3cf3829571a1c221c37e986ed966.json",
"objectKey": "2765094d543818b111d837ea62bad41260a47615c5b99bc608a58e99f24d5b85.json",
"region": "us-east-1",
"assumeRoleArn": "arn:${AWS::Partition}:iam::495395224548:role/cdk-hnb659fds-file-publishing-role-495395224548-us-east-1"
}

View File

@@ -3,21 +3,11 @@
{
"type": "aws:cdk:creationStack",
"data": [
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.js:41:1)",
"...node internals..."
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
}
],
"/AgentClawStack/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C": [
{
"type": "aws:cdk:is-custom-resource-handler-singleton",
"data": true
},
{
"type": "aws:cdk:is-custom-resource-handler-runtime-family",
"data": 2
}
],
"/AgentClawStack/SessionStore": [
{
"type": "aws:cdk:hasPhysicalName",
@@ -26,6 +16,42 @@
}
}
],
"/AgentClawStack/UsersTable": [
{
"type": "aws:cdk:hasPhysicalName",
"data": {
"Ref": "UsersTable9725E9C8"
}
}
],
"/AgentClawStack/WorkspaceMcpFunctionUrl": [
{
"type": "aws:cdk:logicalId",
"data": "WorkspaceMcpFunctionUrl"
},
{
"type": "aws:cdk:creationStack",
"data": [
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:234:5)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
}
],
"/AgentClawStack/GoogleCredentialsSecretArn": [
{
"type": "aws:cdk:logicalId",
"data": "GoogleCredentialsSecretArn"
},
{
"type": "aws:cdk:creationStack",
"data": [
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:238:5)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
}
],
"/AgentClawStack/WebhookUrl": [
{
"type": "aws:cdk:logicalId",
@@ -34,9 +60,9 @@
{
"type": "aws:cdk:creationStack",
"data": [
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.js:188:9)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.js:41:1)",
"...node internals..."
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:243:5)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
}
],
@@ -48,9 +74,9 @@
{
"type": "aws:cdk:creationStack",
"data": [
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.js:192:9)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.js:41:1)",
"...node internals..."
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:248:5)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
}
],
@@ -62,9 +88,23 @@
{
"type": "aws:cdk:creationStack",
"data": [
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.js:196:9)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.js:41:1)",
"...node internals..."
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:253:5)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
}
],
"/AgentClawStack/UsersTableName": [
{
"type": "aws:cdk:logicalId",
"data": "UsersTableName"
},
{
"type": "aws:cdk:creationStack",
"data": [
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:258:5)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
}
],
@@ -76,9 +116,9 @@
{
"type": "aws:cdk:creationStack",
"data": [
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.js:200:9)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.js:41:1)",
"...node internals..."
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:263:5)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
}
],
@@ -90,9 +130,9 @@
{
"type": "aws:cdk:creationStack",
"data": [
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.js:204:9)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.js:41:1)",
"...node internals..."
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:268:5)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
}
],
@@ -104,7 +144,7 @@
{
"type": "aws:cdk:creationStack",
"data": [
"...aws-cdk-lib, node internals, source-map-support...",
"...aws-cdk-lib, node internals, @cspotcode/source-map-support...",
"(no user code in 9007199254740991 frames, use --stack-trace-limit to capture more)"
]
}
@@ -117,41 +157,11 @@
{
"type": "aws:cdk:creationStack",
"data": [
"...aws-cdk-lib, node internals, source-map-support...",
"...aws-cdk-lib, node internals, @cspotcode/source-map-support...",
"(no user code in 9007199254740991 frames, use --stack-trace-limit to capture more)"
]
}
],
"/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.js:69:15)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.js:41:1)",
"...node internals..."
]
}
],
"/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.js:77:13)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.js:41:1)",
"...node internals..."
]
}
],
"/AgentClawStack/SessionStore/Resource": [
{
"type": "aws:cdk:logicalId",
@@ -161,9 +171,24 @@
"type": "aws:cdk:creationStack",
"data": [
"...new Table2 in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.js:83:30)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.js:41:1)",
"...node internals..."
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:60:26)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
}
],
"/AgentClawStack/UsersTable/Resource": [
{
"type": "aws:cdk:logicalId",
"data": "UsersTable9725E9C8"
},
{
"type": "aws:cdk:creationStack",
"data": [
"...new Table2 in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:69:24)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
}
],
@@ -176,9 +201,9 @@
"type": "aws:cdk:creationStack",
"data": [
"...new Queue2 in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.js:91:30)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.js:41:1)",
"...node internals..."
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:77:26)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
}
],
@@ -191,9 +216,9 @@
"type": "aws:cdk:creationStack",
"data": [
"...new Function2 in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.js:99:28)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.js:41:1)",
"...node internals..."
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:86:24)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
}
],
@@ -206,9 +231,9 @@
"type": "aws:cdk:creationStack",
"data": [
"...new Function2 in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.js:115:31)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.js:41:1)",
"...node internals..."
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:103:27)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
}
],
@@ -221,9 +246,9 @@
"type": "aws:cdk:creationStack",
"data": [
"...new HttpApi2 in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.js:147:25)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.js:41:1)",
"...node internals..."
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:141:21)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
}
],
@@ -236,9 +261,9 @@
"type": "aws:cdk:creationStack",
"data": [
"...new Role2 in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.js:160:30)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.js:41:1)",
"...node internals..."
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:159:26)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
}
],
@@ -250,56 +275,11 @@
{
"type": "aws:cdk:creationStack",
"data": [
"...aws-cdk-lib, node internals, source-map-support...",
"...aws-cdk-lib, node internals, @cspotcode/source-map-support...",
"(no user code in 9007199254740991 frames, use --stack-trace-limit to capture more)"
]
}
],
"/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.js:77:13)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.js:41:1)",
"...node internals..."
]
}
],
"/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.js:77:13)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.js:41:1)",
"...node internals..."
]
}
],
"/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.js:77:13)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.js:41:1)",
"...node internals..."
]
}
],
"/AgentClawStack/TgIngest/ServiceRole/Resource": [
{
"type": "aws:cdk:logicalId",
@@ -309,9 +289,9 @@
"type": "aws:cdk:creationStack",
"data": [
"...new Function2 in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.js:99:28)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.js:41:1)",
"...node internals..."
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:86:24)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
}
],
@@ -324,9 +304,9 @@
"type": "aws:cdk:creationStack",
"data": [
"...new Function2 in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.js:115:31)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.js:41:1)",
"...node internals..."
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:103:27)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
}
],
@@ -339,9 +319,9 @@
"type": "aws:cdk:creationStack",
"data": [
"...WrappedClass.addEventSource in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.js:142:23)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.js:41:1)",
"...node internals..."
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:135:19)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
}
],
@@ -354,9 +334,9 @@
"type": "aws:cdk:creationStack",
"data": [
"...new HttpApi2 in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.js:147:25)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.js:41:1)",
"...node internals..."
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:141:21)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
}
],
@@ -371,9 +351,9 @@
".../Users/daniel/agent-claw/cdk/node_modules/aws-cdk-lib/aws-apigatewayv2/lib/http/api.js:1:96 in aws-cdk-lib...",
"Array.map (:)",
"...WrappedClass.<anonymous> in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.js:150:17)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.js:41:1)",
"...node internals..."
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:145:13)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
}
],
@@ -388,9 +368,9 @@
".../Users/daniel/agent-claw/cdk/node_modules/aws-cdk-lib/aws-apigatewayv2/lib/http/api.js:1:96 in aws-cdk-lib...",
"Array.map (:)",
"...WrappedClass.<anonymous> in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.js:150:17)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.js:41:1)",
"...node internals..."
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:145:13)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
}
],
@@ -403,26 +383,24 @@
"type": "aws:cdk:creationStack",
"data": [
"...WrappedClass.<anonymous> in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.js:164:22)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.js:41:1)",
"...node internals..."
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:163:18)",
"<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": [
"/AgentClawStack/WorkspaceMcpRole/Policy/Resource": [
{
"type": "aws:cdk:logicalId",
"data": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF"
"data": "WorkspaceMcpRolePolicy5B8B0072"
},
{
"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.js:77:13)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.js:41:1)",
"...node internals..."
"...SecretBase.grantRead in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:207:29)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
}
],
@@ -435,9 +413,9 @@
"type": "aws:cdk:creationStack",
"data": [
"...WrappedClass.grantSendMessages in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.js:112:22)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.js:41:1)",
"...node internals..."
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:99:18)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
}
],
@@ -450,9 +428,9 @@
"type": "aws:cdk:creationStack",
"data": [
"...WrappedClass.grantReadWriteData in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.js:131:22)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.js:41:1)",
"...node internals..."
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:120:18)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
}
],
@@ -467,9 +445,9 @@
".../Users/daniel/agent-claw/cdk/node_modules/aws-cdk-lib/aws-apigatewayv2/lib/http/api.js:1:96 in aws-cdk-lib...",
"Array.map (:)",
"...WrappedClass.<anonymous> in aws-cdk-lib...",
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.js:150:17)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.js:41:1)",
"...node internals..."
"new AgentClawStack (/Users/daniel/agent-claw/cdk/lib/agent-claw-stack.ts:145:13)",
"<anonymous> (/Users/daniel/agent-claw/cdk/bin/agent-claw.ts:8:1)",
"...node internals, ts-node, ts-node, ts-node..."
]
}
]

View File

@@ -1,238 +1,6 @@
{
"Description": "agent-claw: serverless personal assistant on AgentCore",
"Resources": {
"WorkspaceBucket53E30B92": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BucketEncryption": {
"ServerSideEncryptionConfiguration": [
{
"ServerSideEncryptionByDefault": {
"SSEAlgorithm": "AES256"
}
}
]
},
"BucketName": "agent-claw-workspace-495395224548",
"Tags": [
{
"Key": "aws-cdk:cr-owned:254e75d0",
"Value": "true"
}
]
},
"UpdateReplacePolicy": "Retain",
"DeletionPolicy": "Retain",
"Metadata": {
"aws:cdk:path": "AgentClawStack/WorkspaceBucket/Resource"
}
},
"WorkspaceFilesAwsCliLayer50B6E9D8": {
"Type": "AWS::Lambda::LayerVersion",
"Properties": {
"Content": {
"S3Bucket": "cdk-hnb659fds-assets-495395224548-us-east-1",
"S3Key": "e2659170a0721541efa761a8d5d04d5e36cbbf691c4b15a9053002b7c825055d.zip"
},
"Description": "/opt/awscli/aws"
},
"Metadata": {
"aws:cdk:path": "AgentClawStack/WorkspaceFiles/AwsCliLayer/Resource",
"aws:asset:path": "asset.e2659170a0721541efa761a8d5d04d5e36cbbf691c4b15a9053002b7c825055d.zip",
"aws:asset:is-bundled": false,
"aws:asset:property": "Content"
}
},
"WorkspaceFilesCustomResourceA7FC771F": {
"Type": "Custom::CDKBucketDeployment",
"Properties": {
"ServiceToken": {
"Fn::GetAtt": [
"CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536",
"Arn"
]
},
"SourceBucketNames": [
"cdk-hnb659fds-assets-495395224548-us-east-1"
],
"SourceObjectKeys": [
"d5a4044422f3c0ab39b0d5bfa4e4ea2b1212f0d420a58b542fbc88917d7a676a.zip"
],
"DestinationBucketName": {
"Ref": "WorkspaceBucket53E30B92"
},
"WaitForDistributionInvalidation": true,
"Prune": true,
"OutputObjectKeys": true
},
"UpdateReplacePolicy": "Delete",
"DeletionPolicy": "Delete",
"Metadata": {
"aws:cdk:path": "AgentClawStack/WorkspaceFiles/CustomResource/Default"
}
},
"CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
}
}
],
"Version": "2012-10-17"
},
"ManagedPolicyArns": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
]
]
}
]
},
"Metadata": {
"aws:cdk:path": "AgentClawStack/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole/Resource"
}
},
"CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF": {
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": [
"s3:GetObject*",
"s3:GetBucket*",
"s3:List*"
],
"Effect": "Allow",
"Resource": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":s3:::cdk-hnb659fds-assets-495395224548-us-east-1"
]
]
},
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":s3:::cdk-hnb659fds-assets-495395224548-us-east-1/*"
]
]
}
]
},
{
"Action": [
"s3:GetObject*",
"s3:GetBucket*",
"s3:List*",
"s3:DeleteObject*",
"s3:PutObject",
"s3:PutObjectLegalHold",
"s3:PutObjectRetention",
"s3:PutObjectTagging",
"s3:PutObjectVersionTagging",
"s3:Abort*"
],
"Effect": "Allow",
"Resource": [
{
"Fn::GetAtt": [
"WorkspaceBucket53E30B92",
"Arn"
]
},
{
"Fn::Join": [
"",
[
{
"Fn::GetAtt": [
"WorkspaceBucket53E30B92",
"Arn"
]
},
"/*"
]
]
}
]
}
],
"Version": "2012-10-17"
},
"PolicyName": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF",
"Roles": [
{
"Ref": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265"
}
]
},
"Metadata": {
"aws:cdk:path": "AgentClawStack/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole/DefaultPolicy/Resource"
}
},
"CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Code": {
"S3Bucket": "cdk-hnb659fds-assets-495395224548-us-east-1",
"S3Key": "3423a042b818e31c1e34a19d6689ab2e5f9b70fcbe9e71df66f241b20a200bd9.zip"
},
"Environment": {
"Variables": {
"AWS_CA_BUNDLE": "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem"
}
},
"Handler": "index.handler",
"Layers": [
{
"Ref": "WorkspaceFilesAwsCliLayer50B6E9D8"
}
],
"Role": {
"Fn::GetAtt": [
"CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265",
"Arn"
]
},
"Runtime": "python3.13",
"Timeout": 900
},
"DependsOn": [
"CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF",
"CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265"
],
"Metadata": {
"aws:cdk:path": "AgentClawStack/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/Resource",
"aws:asset:path": "asset.3423a042b818e31c1e34a19d6689ab2e5f9b70fcbe9e71df66f241b20a200bd9",
"aws:asset:is-bundled": false,
"aws:asset:property": "Code"
}
},
"SessionStore8C86EEFE": {
"Type": "AWS::DynamoDB::Table",
"Properties": {
@@ -261,6 +29,30 @@
"aws:cdk:path": "AgentClawStack/SessionStore/Resource"
}
},
"UsersTable9725E9C8": {
"Type": "AWS::DynamoDB::Table",
"Properties": {
"AttributeDefinitions": [
{
"AttributeName": "actor_id",
"AttributeType": "S"
}
],
"BillingMode": "PAY_PER_REQUEST",
"KeySchema": [
{
"AttributeName": "actor_id",
"KeyType": "HASH"
}
],
"TableName": "agent-claw-users"
},
"UpdateReplacePolicy": "Retain",
"DeletionPolicy": "Retain",
"Metadata": {
"aws:cdk:path": "AgentClawStack/UsersTable/Resource"
}
},
"MessageQueue7A3BF959": {
"Type": "AWS::SQS::Queue",
"Properties": {
@@ -356,7 +148,7 @@
"Properties": {
"Code": {
"S3Bucket": "cdk-hnb659fds-assets-495395224548-us-east-1",
"S3Key": "9d7af346bbad17b4c228d09e33a602eedc03747fe1cec1c7c9b7c8723ce74e5d.zip"
"S3Key": "8da48fd743d1e2cb70d8d1935cee795b6f8cf02609db05e2b8f28449be9ef875.zip"
},
"Environment": {
"Variables": {
@@ -385,7 +177,7 @@
],
"Metadata": {
"aws:cdk:path": "AgentClawStack/TgIngest/Resource",
"aws:asset:path": "asset.9d7af346bbad17b4c228d09e33a602eedc03747fe1cec1c7c9b7c8723ce74e5d",
"aws:asset:path": "asset.8da48fd743d1e2cb70d8d1935cee795b6f8cf02609db05e2b8f28449be9ef875",
"aws:asset:is-bundled": false,
"aws:asset:property": "Code"
}
@@ -467,6 +259,44 @@
}
]
},
{
"Action": [
"dynamodb:BatchGetItem",
"dynamodb:Query",
"dynamodb:GetItem",
"dynamodb:Scan",
"dynamodb:ConditionCheckItem",
"dynamodb:BatchWriteItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:DeleteItem",
"dynamodb:DescribeTable"
],
"Effect": "Allow",
"Resource": [
{
"Fn::GetAtt": [
"UsersTable9725E9C8",
"Arn"
]
}
]
},
{
"Action": [
"dynamodb:GetRecords",
"dynamodb:GetShardIterator"
],
"Effect": "Allow",
"Resource": [
{
"Fn::GetAtt": [
"UsersTable9725E9C8",
"Arn"
]
}
]
},
{
"Action": [
"s3:GetObject*",
@@ -476,22 +306,26 @@
"Effect": "Allow",
"Resource": [
{
"Fn::GetAtt": [
"WorkspaceBucket53E30B92",
"Arn"
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":s3:::agent-claw-workspace-495395224548"
]
]
},
{
"Fn::Join": [
"",
[
"arn:",
{
"Fn::GetAtt": [
"WorkspaceBucket53E30B92",
"Arn"
]
"Ref": "AWS::Partition"
},
"/*"
":s3:::agent-claw-workspace-495395224548/*"
]
]
}
@@ -553,20 +387,22 @@
"Properties": {
"Code": {
"S3Bucket": "cdk-hnb659fds-assets-495395224548-us-east-1",
"S3Key": "eeef9ac2146cd644e1727e77104b58bed992e19379d5070de3a05714ff2dba48.zip"
"S3Key": "7053cd1618f5f520a7aac409588128f920d8fe76791c1dbcc65610454d1a5387.zip"
},
"Environment": {
"Variables": {
"SESSION_TABLE_NAME": {
"Ref": "SessionStore8C86EEFE"
},
"WORKSPACE_BUCKET_NAME": {
"Ref": "WorkspaceBucket53E30B92"
},
"WORKSPACE_BUCKET_NAME": "agent-claw-workspace-495395224548",
"TELEGRAM_BOT_TOKEN_SECRET_ARN": "arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/telegram-bot-token-Oq3in3",
"BRAVE_API_KEY_SECRET_ARN": "arn:aws:secretsmanager:us-east-1:495395224548:secret:agent-claw/brave-api-key-uUSgzi",
"RUNTIME_1_ARN": "PLACEHOLDER_SET_AFTER_RUNTIME_DEPLOY",
"AWS_REGION_NAME": "us-east-1"
"RUNTIME_1_ARN": "arn:aws:bedrock-agentcore:us-east-1:495395224548:runtime/agentclaw_agent_claw_main-vTRGIEG6ON",
"AWS_REGION_NAME": "us-east-1",
"USERS_TABLE_NAME": {
"Ref": "UsersTable9725E9C8"
},
"WORKSPACE_MCP_URL": "https://25hugrzw4uwtueeg77jsmft6lq0wunmd.lambda-url.us-east-1.on.aws/mcp"
}
},
"FunctionName": "agent-claw-agent-runner",
@@ -587,7 +423,7 @@
],
"Metadata": {
"aws:cdk:path": "AgentClawStack/AgentRunner/Resource",
"aws:asset:path": "asset.eeef9ac2146cd644e1727e77104b58bed992e19379d5070de3a05714ff2dba48",
"aws:asset:path": "asset.7053cd1618f5f520a7aac409588128f920d8fe76791c1dbcc65610454d1a5387",
"aws:asset:is-bundled": false,
"aws:asset:property": "Code"
}
@@ -752,22 +588,26 @@
"Effect": "Allow",
"Resource": [
{
"Fn::GetAtt": [
"WorkspaceBucket53E30B92",
"Arn"
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":s3:::agent-claw-workspace-495395224548"
]
]
},
{
"Fn::Join": [
"",
[
"arn:",
{
"Fn::GetAtt": [
"WorkspaceBucket53E30B92",
"Arn"
]
"Ref": "AWS::Partition"
},
"/*"
":s3:::agent-claw-workspace-495395224548/*"
]
]
}
@@ -797,6 +637,66 @@
],
"Effect": "Allow",
"Resource": "*"
},
{
"Action": "lambda:InvokeFunctionUrl",
"Condition": {
"StringEquals": {
"lambda:FunctionUrlAuthType": "AWS_IAM"
}
},
"Effect": "Allow",
"Resource": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":lambda:us-east-1:495395224548:function:agent-claw-workspace-mcp"
]
]
},
"Sid": "WorkspaceMcpInvoke"
},
{
"Action": [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
],
"Effect": "Allow",
"Resource": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":secretsmanager:us-east-1:495395224548:secret:agent-claw/google-workspace-credentials-??????"
]
]
}
},
{
"Action": [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
],
"Effect": "Allow",
"Resource": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":secretsmanager:us-east-1:495395224548:secret:agent-claw/google-oauth-client-??????"
]
]
}
}
],
"Version": "2012-10-17"
@@ -812,10 +712,65 @@
"aws:cdk:path": "AgentClawStack/Runtime1Role/DefaultPolicy/Resource"
}
},
"WorkspaceMcpRolePolicy5B8B0072": {
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
],
"Effect": "Allow",
"Resource": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":secretsmanager:us-east-1:495395224548:secret:agent-claw/google-workspace-credentials-??????"
]
]
}
},
{
"Action": [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
],
"Effect": "Allow",
"Resource": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":secretsmanager:us-east-1:495395224548:secret:agent-claw/google-oauth-client-??????"
]
]
}
}
],
"Version": "2012-10-17"
},
"PolicyName": "WorkspaceMcpRolePolicy5B8B0072",
"Roles": [
"agent-claw-workspace-mcp-role"
]
},
"Metadata": {
"aws:cdk:path": "AgentClawStack/WorkspaceMcpRole/Policy/Resource"
}
},
"CDKMetadata": {
"Type": "AWS::CDK::Metadata",
"Properties": {
"Analytics": "v2:deflate64:H4sIAAAAAAAA/22R0U7DMAxFv4X3LIxufMBWQCCBGC3itXJbr8qWJqV2VlVR/x0lZQMhnu7JvY4VO4lMbhO5vIKBFlV9XGhVSp8zVEeRIVnXVyhgoMLTSvqtq47IIt2bb5plC4SToFXha+y0HVs0LOfo7mIIIEImuQkyCQ1tWYP06d48w4j9B/akrBG5Mo1GtubBmYqDc4F0/2Pen9BwHp/3Al2nTBPi/90d9q2i0H0SClrpM6sxBFF3VqtqjHWRJlGPBlpbl9K/QzlXRpgEfZL0bw5dNCNMAjrVAOMA4ymR/pG523Qq5EHCMWdo4oUZgpVZxzM9Gcamh/OAf46xbjpvq9BhUwUMVGklNwOlWsXlibjU0D6O7Ihte/m90OYXvzruHE/C2Brlga5PyVrerOXy6kBKLXpnWLUos1m/AKsec0UeAgAA"
"Analytics": "v2:deflate64:H4sIAAAAAAAA/22PwU7DMAyGn2X31IxuPMCGQHBAjI775KZela1NSu1sqqK8O0rKOCBO/+ff+WO7hPKhhOUCr1zo5lx0poawF9RnVRE7P2pSeOVD4BVsvT6TbJFJNZPF3jU1hE+sO1KPR5shKv5iCB+efDYzRNVhXzcI4dlbLcbZ1PrlpwtZ2edRbzgMxrap/b+7o7E3zCl2y6d9ojLYQ6jcvErWneuMnnIoU1S8OiAzCcMmicLBtCh0xelSQngRGTaDSYEkqdwLtvnDGZJVOS8zvVqhdsTbOX/K/C5GlSel+M8B714GL1FZ1xCc+O5SruF+DcvFiY0pRm/F9ATVrN8RDS1cnQEAAA=="
},
"Metadata": {
"aws:cdk:path": "AgentClawStack/CDKMetadata/Default"
@@ -823,6 +778,25 @@
}
},
"Outputs": {
"WorkspaceMcpFunctionUrl": {
"Description": "workspace-mcp Lambda Function URL (MCP endpoint for Gmail/Calendar)",
"Value": "https://25hugrzw4uwtueeg77jsmft6lq0wunmd.lambda-url.us-east-1.on.aws"
},
"GoogleCredentialsSecretArn": {
"Description": "Google OAuth user credentials secret ARN",
"Value": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":secretsmanager:us-east-1:495395224548:secret:agent-claw/google-workspace-credentials"
]
]
}
},
"WebhookUrl": {
"Description": "Register this URL with Telegram BotFather as webhook endpoint",
"Value": {
@@ -844,9 +818,7 @@
},
"WorkspaceBucketName": {
"Description": "S3 bucket containing agent workspace files",
"Value": {
"Ref": "WorkspaceBucket53E30B92"
}
"Value": "agent-claw-workspace-495395224548"
},
"SessionTableName": {
"Description": "DynamoDB table for session mapping",
@@ -854,6 +826,12 @@
"Ref": "SessionStore8C86EEFE"
}
},
"UsersTableName": {
"Description": "DynamoDB user registry table",
"Value": {
"Ref": "UsersTable9725E9C8"
}
},
"MessageQueueUrl": {
"Description": "SQS FIFO queue for incoming messages",
"Value": {

View File

@@ -0,0 +1,151 @@
import json
import os
import time
import uuid
import boto3
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:
_agentcore = boto3.client('bedrock-agentcore', region_name='us-east-1')
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),
'allowed': True,
}
table.put_item(Item=item)
print(f'[agent-runner] Registered new user: {actor_id}')
return item
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', '')
actor_id = f"{channel}:{chat_id}"
# ── User registry ─────────────────────────────────────────────────────
from_info = first.get('messages', [{}])[0]
user_profile = get_or_create_user(actor_id, from_info)
# ── 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)
# ── 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', ''),
'allowed': user_profile.get('allowed', True),
},
'channel_adapter': {
'type': channel,
'target_id': str(chat_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(),
)
# Consume streaming response (agent delivers to Telegram via send_message tool)
for chunk in response.get('response', []):
pass # intentional no-op — agent handles delivery internally
print(f"[agent-runner] Completed session={session_id} actor={actor_id}")

View File

@@ -0,0 +1,101 @@
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()
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) -> None:
"""Fire-and-forget typing action (does not raise on failure)."""
try:
token = get_bot_token()
data = json.dumps({'chat_id': chat_id, 'action': 'typing'}).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 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', ''))
text = message.get('text', '')
from_user = message.get('from', {})
timestamp = message.get('date', 0)
print(f'[tg-ingest] chat_id={chat_id} text_len={len(text)} update_id={update_id}')
if not chat_id or not text:
print(f'[tg-ingest] Dropping: chat_id={chat_id!r} text={text!r}')
return {'statusCode': 200, 'body': 'ok'}
# ── Send typing action (non-blocking, background thread) ──────────────
t = threading.Thread(target=send_typing, args=(chat_id,))
t.daemon = True
t.start()
# ── Enqueue to SQS FIFO ───────────────────────────────────────────────
sqs = boto3.client('sqs')
sqs.send_message(
QueueUrl=os.environ['MESSAGE_QUEUE_URL'],
MessageGroupId=chat_id,
MessageDeduplicationId=str(update_id),
MessageBody=json.dumps({
'channel': 'telegram',
'chat_id': chat_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,
}),
)
return {'statusCode': 200, 'body': 'ok'}

View File

@@ -18,7 +18,7 @@
"validateOnSynth": false,
"assumeRoleArn": "arn:${AWS::Partition}:iam::495395224548:role/cdk-hnb659fds-deploy-role-495395224548-us-east-1",
"cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::495395224548:role/cdk-hnb659fds-cfn-exec-role-495395224548-us-east-1",
"stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-495395224548-us-east-1/d7e0fade0cb46eefc22ea1239ac2735f5c6d3cf3829571a1c221c37e986ed966.json",
"stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-495395224548-us-east-1/2765094d543818b111d837ea62bad41260a47615c5b99bc608a58e99f24d5b85.json",
"requiresBootstrapStackVersion": 6,
"bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version",
"additionalDependencies": [

File diff suppressed because one or more lines are too long

View File

@@ -65,6 +65,14 @@ export class AgentClawStack extends cdk.Stack {
removalPolicy: cdk.RemovalPolicy.RETAIN,
});
// ── DynamoDB user registry ─────────────────────────────────────────────
const usersTable = new dynamodb.Table(this, 'UsersTable', {
tableName: 'agent-claw-users',
partitionKey: { name: 'actor_id', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
removalPolicy: cdk.RemovalPolicy.RETAIN,
});
// ── SQS FIFO message queue ─────────────────────────────────────────────
const messageQueue = new sqs.Queue(this, 'MessageQueue', {
queueName: 'agent-claw-messages.fifo',
@@ -110,6 +118,8 @@ export class AgentClawStack extends cdk.Stack {
});
sessionTable.grantReadWriteData(agentRunnerFn);
usersTable.grantReadWriteData(agentRunnerFn);
agentRunnerFn.addEnvironment('USERS_TABLE_NAME', usersTable.tableName);
workspaceBucket.grantRead(agentRunnerFn);
botTokenSecret.grantRead(agentRunnerFn);
braveApiKeySecret.grantRead(agentRunnerFn);
@@ -245,6 +255,11 @@ export class AgentClawStack extends cdk.Stack {
description: 'DynamoDB table for session mapping',
});
new cdk.CfnOutput(this, 'UsersTableName', {
value: usersTable.tableName,
description: 'DynamoDB user registry table',
});
new cdk.CfnOutput(this, 'MessageQueueUrl', {
value: messageQueue.queueUrl,
description: 'SQS FIFO queue for incoming messages',

View File

@@ -24,6 +24,29 @@ def get_agentcore():
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),
'allowed': True,
}
table.put_item(Item=item)
print(f'[agent-runner] Registered new user: {actor_id}')
return item
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'])
@@ -72,9 +95,13 @@ def handler(event, context):
chat_id = first.get('chat_id', '')
actor_id = f"{channel}:{chat_id}"
# ── User registry ─────────────────────────────────────────────────────
from_info = first.get('messages', [{}])[0]
user_profile = get_or_create_user(actor_id, from_info)
# ── Get or create AgentCore session ──────────────────────────────────
session_id = get_or_create_session(actor_id)
print(f"[agent-runner] actor={actor_id} session={session_id}")
print(f"[agent-runner] actor={actor_id} session={session_id} user={user_profile.get('display_name', '')}")
# ── Bundle messages ───────────────────────────────────────────────────
if len(records) == 1:
@@ -91,6 +118,11 @@ def handler(event, context):
'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', ''),
'allowed': user_profile.get('allowed', True),
},
'channel_adapter': {
'type': channel,
'target_id': str(chat_id),