feat(aws): add ECS Fargate deployment for Clawdbot Discord bot

Add complete ECS Fargate infrastructure for Clawdbot Discord bot
deployment on AWS.

## Infrastructure (Terraform)
- **VPC**: 100.64.0.0/16 with DNS support
- **Public Subnets**: 2 subnets in different AZs
- **ECR Repository**: clawdbot/bot with lifecycle policy
- **IAM Roles**: Task Role (DynamoDB+S3) + Execution Role
- **ECS Resources**: Fargate (256 CPU, 2048 MB memory)
- **Security Group**: Outbound only
- **CloudWatch**: Log group with 7-day retention

## Security Improvements
- Discord Bot Token → AWS Secrets Manager
- IAM policy with least privilege principle
- No plaintext secrets in code

## Code Quality
- Fix all 22 lint errors
- Add *.d.ts to oxlint ignore patterns
- Fix Dockerfile .buildstamp for Linux compatibility

Closes #2049

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Shunsuke Hayashi 2026-01-26 13:38:43 +09:00
parent 3c63ee1d6d
commit 4d74fdf593
45 changed files with 1837 additions and 103 deletions

93
.claude/DISCORD_NOTIFY.md Normal file
View File

@ -0,0 +1,93 @@
# Clawdbot Discord 通知設定
## 概要
Clawdbot の Discord コネクションを使用して、Claude Code の操作完了時に Discord 通知を送信します。
## 設定手順
### 1. Discord チャンネル ID を取得
通知を送信したい Discord チャンネルの ID を取得します:
1. Discord の設定 → 詳細設定 → 開発者モード をオン
2. チャンネルを右クリック → ID をコピー
3. チャンネル ID: `123456789012345678` のような形式
### 2. .env に設定
```bash
cd ~/dev/clawdbot
echo "DISCORD_NOTIFY_CHANNEL=123456789012345678" >> .env
```
### 3. Clawdbot Discord 接続を設定
```bash
# Discord アカウント連携
clawdbot login --channel discord
# 接続確認
clawdbot channels status --probe
```
### 4. 動作確認
```bash
# テスト通知
clawdbot message send \
--channel discord \
--target "123456789012345678" \
--message "🤖 テスト通知"
```
## 通知タイミング
| タイミング | 内容 |
|-----------|------|
| PostToolUse | 操作完了時に Discord 通知を送信 |
## 環境変数
| 変数 | 説明 | 例 |
|------|------|-----|
| `DISCORD_NOTIFY_CHANNEL` | 通知先 Discord チャンネル ID | `123456789012345678` |
| `CLAWDBOT_DIR` | clawdbot のパス(任意) | `$HOME/dev/clawdbot` |
## ファイル構成
```
.claude/
├── hooks/
│ └── discord-notify-clawdbot.sh # Discord 通知スクリプト
├── settings.json # Claude Code フック設定
└── DISCORD_NOTIFY.md # このドキュメント
```
## トラブルシューティング
### 通知が届かない場合
1. Discord 接続状態確認:
```bash
clawdbot channels status
```
2. チャンネル ID 確認:
```bash
grep DISCORD_NOTIFY_CHANNEL .env
```
3. 手動テスト:
```bash
.claude/hooks/discord-notify-clawdbot.sh << EOF
{"content": "テスト通知"}
EOF
```
### CLI がビルドされていない場合
```bash
cd ~/dev/clawdbot
pnpm build
```

170
.claude/SECOND_BRAIN.md Normal file
View File

@ -0,0 +1,170 @@
# 🧠 Discord セカンドブレイン設定
## 概要
Claude Code のセッション情報を Discord に保存し、**検索可能なナレッジベース**として活用します。
---
## 🎯 保存される情報
### セッションメタデータ
| 項目 | 説明 |
|------|------|
| 日時 | セッションの日時 |
| ユーザー | 誰が作業したか |
| ホスト | どのマシンで作業したか |
| プロジェクト | 作業対象プロジェクト |
| パス | ワークディレクトリ |
### セクション構成
```markdown
# 🧠 Claude Code Memory
## 💡 重要な発見・学習
このセッションで得た知見
## 🔧 技術的メモ
技術的な詳細・設定変更など
## ⚠️ 問題・エラーと解決策
遭遇した問題と解決方法
## 🔗 関連リソース
参考ドキュメント・Issueなど
## 📂 Git 変更
変更ファイル一覧
```
---
## 📁 保存場所
| 場所 | 説明 |
|------|------|
| **Discord** | `#general` (デフォルト) - 全体で共有 |
| **ローカル** | `~/.clawdbot/memory/YYYY-MM-DD-HHMMSS.md` |
---
## 🔧 設定
### 環境変数
| 変数 | 説明 | デフォルト |
|------|------|----------|
| `DISCORD_BRAIN_CHANNEL` | 保存先チャンネルID | `1465087447225598207` (#general) |
| `DISCORD_NOTIFY_CHANNEL` | ステータス通知先 | `1465087451113722019` (#status) |
### チャンネル変更例
```bash
cd ~/dev/clawdbot
# #announcements に保存
echo 'DISCORD_BRAIN_CHANNEL=1465087454963830825' >> .env
# #task-queue に保存
echo 'DISCORD_BRAIN_CHANNEL=1465087503622209757' >> .env
```
---
## 📊 フック実行順序
```
SessionStart → タスク開始通知
[作業実行]
SessionEnd → 1. セカンドブレイン保存 (discord-brain-save.sh)
→ 2. タスク完了通知 (discord-task-end.sh)
```
---
## 🔍 検索・活用方法
### Discord 検索
```
# プロジェクト別
in:#general #clawcode
# 日付別
in:#general from:bot after:2026-01-26
# キーワード検索
in:#general エラー 解決
in:#general 設定 変更
```
### ローカル検索
```bash
# メモリディレクトリから検索
cd ~/.clawdbot/memory
grep -r "キーワード" *.md
```
---
## ✨ 活用シナリオ
### 1. 問題解決の記録
- エラー内容と解決手順
- トラブルシューティングのナレッジ蓄積
### 2. 設定変更の履歴
- どの設定をいつ変更したか
- 変更前後の比較
### 3. 学習の記録
- 新しく学んだ技術・概念
- ベストプラクティスの発見
### 4. チーム共有
- 全員が同じ Discord を見れる
- 知見の共有・再利用
---
## 📝 セカンドブレイン運用ガイド
### 効率的なタグ付け
Discord のメッセージには自動的にタグが付与されます:
- `#clawcode` - Claude Code セッション
- `#memory` - メモリ記録
- `#プロジェクト名` - プロジェクト特定
### 手動追加推奨タグ
```
#解決済 - 問題解決記録
#設定 - 設定変更
#バグ - バグ報告
#学習 - 新規学習
```
---
## 🔄 保管・アーカイブ
古いメモリの整理:
1. 定期的に Discord メッセージをスレッド化
2. 重要な情報は Wiki に昇格
3. ローカルファイルは適宜削除・アーカイブ
---
準備完了です!次回のセッション終了時に自動保存されます。

43
.claude/SESSION_NOTIFY.md Normal file
View File

@ -0,0 +1,43 @@
# Clawdbot セッション終了通知
## 設定完了
**セッション終了時のみ Discord 通知** が設定されました
### 通知内容
```
🤖 Claude Code セッション終了
📅 日時: 2026-01-26 10:30:00
👤 ユーザー: shunsukehayashi
🖥️ ホスト: Mac-mini
✅ 作業完了
```
### 設定値
| 項目 | 値 |
|------|-----|
| 通知チャンネル | **#status** |
| チャンネル ID | `1465087451113722019` |
| 通知タイミング | セッション終了時1回のみ |
### チャンネル変更
通知先を変更する場合:
```bash
cd ~/dev/clawdbot
# #general に変更
echo 'DISCORD_NOTIFY_CHANNEL=1465087447225598207' >> .env
# #task-queue に変更
echo 'DISCORD_NOTIFY_CHANNEL=1465087503622209757' >> .env
```
### テスト
セッションを終了して通知を確認してください。

86
.claude/TASK_TRACKING.md Normal file
View File

@ -0,0 +1,86 @@
# Clawdbot タスク追跡通知
## ✅ 設定完了
**セッション開始・終了時の Discord 通知**が設定されました。
---
## 📊 通知内容
### セッション開始時 (SessionStart)
```
🚀 Claude Code セッション開始
📅 開始時刻: 2026-01-26 10:30:00
👤 ユーザー: shunsukehayashi
🖥️ ホスト: Mac-mini
📁 ワークディレクトリ: ~/dev/clawdbot
⏳ 作業開始...
```
### セッション終了時 (SessionEnd)
```
✅ Claude Code セッション完了
📅 開始時刻: 2026-01-26 10:30:00
📅 完了時刻: 2026-01-26 11:45:00
⏱️ 経過時間: 1時間15分
👤 ユーザー: shunsukehayashi
🖥️ ホスト: Mac-mini
📂 Git: feature/xxx (5ファイル変更)
✨ 作業完了
```
---
## 🔧 設定値
| 項目 | 値 |
|------|-----|
| 通知チャンネル | **#status** |
| チャンネル ID | `1465087451113722019` |
| 通知タイミング | セッション開始時 + 終了時 |
---
## 📁 ファイル構成
```
.claude/
├── hooks/
│ ├── discord-task-start.sh # セッション開始通知
│ └── discord-task-end.sh # セッション終了通知
├── settings.json # Claude Code フック設定
└── TASK_TRACKING.md # このドキュメント
```
---
## 🧪 テスト方法
1. 新しい Claude Code セッションを開始
2. Discord #status チャンネルで開始通知を確認
3. セッションを終了
4. 完了通知(経過時間付き)を確認
---
## 🔍 追跡される情報
| 項目 | 説明 |
|------|------|
| 開始/完了時刻 | 正確な作業時間 |
| 経過時間 | 自動計算(時間/分/秒) |
| ユーザー名 | 誰が作業したか |
| ホスト名 | どのマシンで作業したか |
| Git状態 | ブランチ名 + 変更ファイル数 |
| ワークディレクトリ | 作業場所 |
---
準備完了です!次回の Claude Code セッションから通知が届きます。

View File

@ -0,0 +1,178 @@
#!/bin/bash
# Clawdbot Discord Second Brain Hook
# SessionEnd: Save important insights to Discord (Knowledge Base)
# clawdbot のパス
CLAWDBOT_DIR="${CLAWDBOT_DIR:-$HOME/dev/clawdbot}"
CLAWDBOT_CLI="$CLAWDBOT_DIR/dist/entry.js"
# CLIが存在するか確認
[ ! -f "$CLAWDBOT_CLI" ] && exit 0
# Discord 設定
# セカンドブレイン用チャンネル(別途設定可能)
BRAIN_CHANNEL_ID="${DISCORD_BRAIN_CHANNEL:-1465087447225598207}" # デフォルト: #general
DISCORD_ACCOUNT_ID="${DISCORD_NOTIFY_ACCOUNT:-ppal}"
# メモリ保存先
MEMORY_FILE="/tmp/clawdbot-brain-${USER}-$$.md"
# クリーンアップ用trap
trap "rm -f $MEMORY_FILE" EXIT INT TERM
# セッション情報収集
SESSION_DATE=$(date '+%Y-%m-%d')
SESSION_TIME=$(date '+%H:%M:%S')
SESSION_HOST=$(hostname -s 2>/dev/null || hostname | cut -d'.' -f1)
SESSION_PWD=$PWD
# Git 変更情報収集
GIT_INFO=""
if git rev-parse --git-dir > /dev/null 2>&1; then
GIT_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
GIT_CHANGED=$(git status --short 2>/dev/null)
GIT_COMMIT=$(git log -1 --pretty=format:"%h - %s" 2>/dev/null || echo "no commit")
if [ -n "$GIT_CHANGED" ]; then
GIT_INFO="
## 📂 Git 変更
\`\`\`
$GIT_CHANGED
\`\`\`
**ブランチ**: $GIT_BRANCH
**最新コミット**: $GIT_COMMIT"
fi
fi
# 作業内容推測(ディレクトリ名から)
PROJECT_NAME=$(basename "$SESSION_PWD")
WORK_CONTEXT=""
case "$PROJECT_NAME" in
*clawdbot*)
WORK_CONTEXT="🦞 Clawdbot 開発作業"
;;
*miyabi*)
WORK_CONTEXT="🎭 Miyabi エージェント作業"
;;
*ppal*)
WORK_CONTEXT="📚 PPAL コース作業"
;;
gen-studio*)
WORK_CONTEXT="🎨 Gen-Studio 開発"
;;
*)
WORK_CONTEXT="💻 $PROJECT_NAME 作業"
;;
esac
# メモリファイル作成
cat > "$MEMORY_FILE" << EOF
# 🧠 Claude Code Memory - $SESSION_DATE
## 📋 セッション情報
- **日時**: $SESSION_DATE $SESSION_TIME
- **ユーザー**: ${USER:-unknown}
- **ホスト**: $SESSION_HOST
- **プロジェクト**: $WORK_CONTEXT
- **パス**: \`$SESSION_PWD\`
---
## 💡 重要な発見・学習
<!-- このセッションで得た知見を記述 -->
- 追加あり次更新
---
## 🔧 技術的メモ
<!-- 技術的な詳細・設定変更など -->
- 追加あり次更新
---
## ⚠️ 問題・エラーと解決策
<!-- 遭遇した問題と解決方法 -->
- なし
---
## 🔗 関連リソース
<!-- 参考ドキュメント・Issueなど -->
- 追加あり次更新
---
$GIT_INFO
**タグ**: #clawcode #memory #${PROJECT_NAME//-/}
EOF
# Discord に送信(コードブロックとして整形)
BRAIN_MESSAGE="🧠 **セカンドブレインに保存**
\`\`\`markdown
$(cat "$MEMORY_FILE")
\`\`\`
---
💾 **記録完了**: ${SESSION_DATE} ${SESSION_TIME}"
# jq が利用可能かチェック
USE_JQ=0
if command -v jq >/dev/null 2>&1; then
USE_JQ=1
fi
# Discord 送信
if [ -n "$DISCORD_WEBHOOK_URL" ]; then
if [ $USE_JQ -eq 1 ]; then
JSON_CONTENT=$(echo "$BRAIN_MESSAGE" | jq -Rs .)
else
# フォールバック: 簡易エスケープ
JSON_CONTENT=$(echo "$BRAIN_MESSAGE" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | tr '\n' ' ')
fi
(
curl -s -X POST "$DISCORD_WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "{\"content\": $JSON_CONTENT}" \
>/dev/null 2>&1
) &
elif [ -n "$DISCORD_ACCOUNT_ID" ]; then
(
# バイト数で判定Discordの制限は2000バイト
MESSAGE_BYTES=$(printf "%s" "$BRAIN_MESSAGE" | wc -c | tr -d ' ')
if [ "$MESSAGE_BYTES" -gt 1900 ]; then
# 短縮版メッセージ
node "$CLAWDBOT_CLI" message send \
--channel discord \
--account "$DISCORD_ACCOUNT_ID" \
--target "$BRAIN_CHANNEL_ID" \
--message "🧠 **セカンドブレインに保存**
セッションメモリが生成されました:
- 日時: ${SESSION_DATE} ${SESSION_TIME}
- プロジェクト: $WORK_CONTEXT
- サイズ: ${MESSAGE_BYTES}バイト
詳細はローカルファイルを確認してください。" \
>/dev/null 2>&1
else
node "$CLAWDBOT_CLI" message send \
--channel discord \
--account "$DISCORD_ACCOUNT_ID" \
--target "$BRAIN_CHANNEL_ID" \
--message "$BRAIN_MESSAGE" \
>/dev/null 2>&1
fi
) &
fi
# ローカルにも保存(検索用)
MEMORY_DIR="$HOME/.clawdbot/memory"
mkdir -p "$MEMORY_DIR"
MEMORY_LOCAL="$MEMORY_DIR/${SESSION_DATE}-${USER}-$(date '+%H%M%S').md"
cp "$MEMORY_FILE" "$MEMORY_LOCAL"
exit 0

View File

@ -0,0 +1,96 @@
#!/bin/bash
# Clawdbot Discord Task End Notification Hook
# SessionEnd: Report task completion
# clawdbot のパス
CLAWDBOT_DIR="${CLAWDBOT_DIR:-$HOME/dev/clawdbot}"
CLAWDBOT_CLI="$CLAWDBOT_DIR/dist/entry.js"
# CLIが存在するか確認
[ ! -f "$CLAWDBOT_CLI" ] && exit 0
# Discord 設定
DISCORD_CHANNEL_ID="${DISCORD_NOTIFY_CHANNEL:-1465087451113722019}" # #status
DISCORD_ACCOUNT_ID="${DISCORD_NOTIFY_ACCOUNT:-ppal}"
# セッションIDPPIDを使用してサブシェルでも一貫性を確保
SESSION_ID="${CLAUDE_SESSION_ID:-${PPID:-$$}}"
# タスク情報ファイルを読み込み
TASK_INFO_FILE="/tmp/clawdbot-task-info-${USER}-${SESSION_ID}.txt"
TASK_START_TIME="不明"
if [ -f "$TASK_INFO_FILE" ]; then
source "$TASK_INFO_FILE"
rm -f "$TASK_INFO_FILE"
fi
# 経過時間計算GNU/BSD date両対応
if [ -n "$TASK_START_TIME" ] && [ "$TASK_START_TIME" != "不明" ]; then
# dateコマンドの種類を検出
if date --version >/dev/null 2>&1; then
# GNU date (Linux)
START_SEC=$(date -d "$TASK_START_TIME" "+%s" 2>/dev/null || echo "0")
else
# BSD date (macOS)
START_SEC=$(date -j -f "%Y-%m-%d %H:%M:%S" "$TASK_START_TIME" "+%s" 2>/dev/null || echo "0")
fi
END_SEC=$(date "+%s")
ELAPSED=$((END_SEC - START_SEC))
if [ $ELAPSED -gt 0 ]; then
if [ $ELAPSED -ge 3600 ]; then
DURATION="$((ELAPSED / 3600))時間$((ELAPSED % 3600 / 60))"
elif [ $ELAPSED -ge 60 ]; then
DURATION="$((ELAPSED / 60))$((ELAPSED % 60))"
else
DURATION="${ELAPSED}"
fi
else
DURATION="計測不可"
fi
else
DURATION="不明"
fi
# Git 変更サマリーgitリポジトリの場合
GIT_SUMMARY=""
if git rev-parse --git-dir > /dev/null 2>&1; then
BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
CHANGED_FILES=$(git status --short 2>/dev/null | wc -l | tr -d ' ')
GIT_SUMMARY="
📂 Git: $BRANCH (${CHANGED_FILES}ファイル変更)"
fi
# セッション終了メッセージ
END_MESSAGE="✅ **Claude Code セッション完了**
📅 開始時刻: $TASK_START_TIME
📅 完了時刻: $(date '+%Y-%m-%d %H:%M:%S')
⏱️ 経過時間: ${DURATION}
👤 ユーザー: ${USER:-unknown}
🖥️ ホスト: $(hostname -s 2>/dev/null || hostname | cut -d'.' -f1)${GIT_SUMMARY}
✨ 作業完了"
# Discord 送信
if [ -n "$DISCORD_WEBHOOK_URL" ]; then
(
curl -s -X POST "$DISCORD_WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "{\"content\": \"$END_MESSAGE\"}" \
>/dev/null 2>&1
) &
elif [ -n "$DISCORD_ACCOUNT_ID" ]; then
(
node "$CLAWDBOT_CLI" message send \
--channel discord \
--account "$DISCORD_ACCOUNT_ID" \
--target "$DISCORD_CHANNEL_ID" \
--message "$END_MESSAGE" \
>/dev/null 2>&1
) &
fi
exit 0

View File

@ -0,0 +1,64 @@
#!/bin/bash
# Clawdbot Discord Task Start Notification Hook
# SessionStart: Declare task start
# clawdbot のパス
CLAWDBOT_DIR="${CLAWDBOT_DIR:-$HOME/dev/clawdbot}"
CLAWDBOT_CLI="$CLAWDBOT_DIR/dist/entry.js"
# CLIが存在するか確認
[ ! -f "$CLAWDBOT_CLI" ] && exit 0
# Discord 設定
DISCORD_CHANNEL_ID="${DISCORD_NOTIFY_CHANNEL:-1465087451113722019}" # #status
DISCORD_ACCOUNT_ID="${DISCORD_NOTIFY_ACCOUNT:-ppal}"
# セッションIDPPIDを使用してサブシェルでも一貫性を確保
SESSION_ID="${CLAUDE_SESSION_ID:-${PPID:-$$}}"
# タスク情報保存先
TASK_INFO_FILE="/tmp/clawdbot-task-info-${USER}-${SESSION_ID}.txt"
# クリーンアップ用trap
trap "rm -f $TASK_INFO_FILE" EXIT INT TERM
# タスク開始情報を保存
cat > "$TASK_INFO_FILE" << EOF
TASK_START_TIME=$(date '+%Y-%m-%d %H:%M:%S')
TASK_USER=${USER:-unknown}
TASK_HOST=$(hostname)
TASK_PWD=$PWD
TASK_SESSION_ID=${SESSION_ID}
EOF
# セッション開始メッセージ
START_MESSAGE="🚀 **Claude Code セッション開始**
📅 開始時刻: $(date '+%Y-%m-%d %H:%M:%S')
👤 ユーザー: ${USER:-unknown}
🖥️ ホスト: $(hostname -s 2>/dev/null || hostname | cut -d'.' -f1)
📁 ワークディレクトリ: $PWD
🆔 セッションID: ${SESSION_ID}
⏳ 作業開始..."
# Discord 送信
if [ -n "$DISCORD_WEBHOOK_URL" ]; then
(
curl -s -X POST "$DISCORD_WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "{\"content\": \"$START_MESSAGE\"}" \
>/dev/null 2>&1
) &
elif [ -n "$DISCORD_ACCOUNT_ID" ]; then
(
node "$CLAWDBOT_CLI" message send \
--channel discord \
--account "$DISCORD_ACCOUNT_ID" \
--target "$DISCORD_CHANNEL_ID" \
--message "$START_MESSAGE" \
>/dev/null 2>&1
) &
fi
exit 0

32
.claude/settings.json Normal file
View File

@ -0,0 +1,32 @@
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": ".claude/hooks/discord-task-start.sh",
"timeout": 10
}
]
}
],
"SessionEnd": [
{
"hooks": [
{
"type": "command",
"command": ".claude/hooks/discord-brain-save.sh",
"timeout": 15
},
{
"type": "command",
"command": ".claude/hooks/discord-task-end.sh",
"timeout": 10
}
]
}
]
}
}

1
.env.bak Normal file
View File

@ -0,0 +1 @@
DISCORD_NOTIFY_CHANNEL=1260121338811514880

View File

@ -3,3 +3,16 @@ TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_AUTH_TOKEN=your_auth_token_here
# Must be a WhatsApp-enabled Twilio number, prefixed with whatsapp:
TWILIO_WHATSAPP_FROM=whatsapp:+17343367101
# Discord Webhook (for simple notifications)
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/xxx/yyy
# Discord Notification Channel (for clawdbot connection)
# Default: #status channel (PPAL Server)
# Use Discord channel ID (e.g., "1465087451113722019")
DISCORD_NOTIFY_CHANNEL=1465087451113722019
# Discord Second Brain Channel (for memory/knowledge storage)
# Default: #general channel (PPAL Server)
# Use Discord channel ID (e.g., "1465087447225598207")
DISCORD_BRAIN_CHANNEL=1465087447225598207

8
.gitignore vendored
View File

@ -76,3 +76,11 @@ src/**/*.js.map
src/**/*.d.ts
src/**/*.d.ts.map
# Terraform
workers/clawdbot-aws/terraform/.terraform/
workers/clawdbot-aws/terraform/*.tfstate
workers/clawdbot-aws/terraform/*.tfstate.backup
workers/clawdbot-aws/terraform/*.tfplan
workers/clawdbot-aws/terraform/plan.tfplan
workers/clawdbot-aws/terraform/tfplan

View File

@ -8,5 +8,5 @@
"categories": {
"correctness": "error"
},
"ignorePatterns": ["src/canvas-host/a2ui/a2ui.bundle.js"]
"ignorePatterns": ["src/canvas-host/a2ui/a2ui.bundle.js", "**/*.d.ts"]
}

0
.tmp Normal file
View File

View File

@ -25,6 +25,8 @@ RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
# Create .buildstamp to prevent runtime rebuild (portable timestamp)
RUN mkdir -p dist && echo "$(date +%s)000000000" > dist/.buildstamp
# Force pnpm for UI build (Bun may fail on ARM/Synology architectures)
ENV CLAWDBOT_PREFER_PNPM=1
RUN pnpm ui:install

View File

@ -0,0 +1,76 @@
#!/usr/bin/env node
/**
* List Discord Channels
*
*/
import { listGuildChannelsDiscord } from "../src/discord/send.guild.js";
import { loadConfig } from "../src/config/config.js";
import { resolveDiscordAccount } from "../src/discord/accounts.js";
import { createDiscordClient } from "../src/discord/send.shared.js";
async function main() {
const GUILD_ID = "1260121338811514880"; // PPAL Server
const cfg = loadConfig();
const accountInfo = resolveDiscordAccount({ cfg, accountId: "ppal" });
const { token } = createDiscordClient({}, cfg);
console.log(`\n📋 Discord Server: ${GUILD_ID}`);
console.log(`🤖 Bot: ${accountInfo.accountId} (${accountInfo.config.name || "PPAL Bot"})\n`);
try {
const channels = await listGuildChannelsDiscord(GUILD_ID, { token });
console.log("📺 チャンネル一覧:\n");
console.log("".padEnd(25), "タイプ".padEnd(15), "ID");
console.log("=".repeat(70));
for (const channel of channels) {
const type =
channel.type === 0 ? "テキスト" :
channel.type === 2 ? "音声" :
channel.type === 4 ? "カテゴリ" :
channel.type === 5 ? " announcements" :
channel.type === 15 ? "forum" : `type_${channel.type}`;
const indent = channel.type === 4 ? "" : " ";
console.log(
`${indent}${(channel.name || "").padEnd(25)} ` +
`${type.padEnd(15)} ` +
`${channel.id}`
);
}
console.log("\n");
// テキストチャンネルのみ抽出
const textChannels = channels.filter(ch => ch.type === 0);
console.log(`\n📝 テキストチャンネル (${textChannels.length}件):\n`);
textChannels.forEach(ch => {
console.log(` #${ch.name} → ID: ${ch.id}`);
});
console.log("\n✅ おすすめの通知チャンネル:\n");
const generalChannel = textChannels.find(ch =>
ch.name.toLowerCase().includes("general") ||
ch.name.toLowerCase().includes("通知") ||
ch.name.toLowerCase().includes("notify")
);
if (generalChannel) {
console.log(` 👉 #${generalChannel.name} (ID: ${generalChannel.id})`);
} else if (textChannels.length > 0) {
console.log(` 👉 #${textChannels[0].name} (ID: ${textChannels[0].id})`);
}
console.log("");
} catch (error: any) {
console.error("❌ エラー:", error.message);
process.exit(1);
}
}
main();

View File

@ -3,18 +3,6 @@
*/
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
save,
saveFile,
getDownloadUrl,
get,
listBySession,
listByUser,
deleteArtifact,
deleteBySession,
initializeTable,
} from "./manager.js";
import { ArtifactType } from "./types.js";
// AWS SDKモック
vi.mock("@aws-sdk/client-s3");
@ -23,9 +11,6 @@ vi.mock("@aws-sdk/lib-dynamodb");
vi.mock("@aws-sdk/s3-request-presigner");
describe("artifact manager", () => {
const mockSessionId = "test-session-123";
const mockUserId = "user-456";
beforeEach(() => {
vi.clearAllMocks();
});

View File

@ -25,7 +25,6 @@ import type {
ArtifactType,
SaveArtifactOptions,
DownloadUrlOptions,
ArtifactFilter,
} from "./types.js";
/** デフォルトTTL (24時間) */

View File

@ -3,22 +3,11 @@
*/
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { sendReply, createDiscordReply } from "./discord-reply.js";
import { ResponseFormat } from "./types.js";
// Carbonモック
vi.mock("@buape/carbon");
describe("discord-reply", () => {
const mockApi = {
rest: {
post: vi.fn().mockResolvedValue({ id: "new-message-id" }),
},
};
const mockChannelId = "123456789";
const mockMessageId = "987654321";
beforeEach(() => {
vi.clearAllMocks();
});

View File

@ -7,7 +7,6 @@
import type { Message } from "@buape/carbon";
import type { ReplyOptions, ReplyData, ReplyEmbed, QuoteMetadata, ReplyAuthor } from "./types.js";
import { ResponseFormat } from "./types.js";
/**
* Discordリプライオプション拡張

View File

@ -4,7 +4,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
runCodexReview,
parseCodexOutput,
createReviewRequest,
evaluateReview,

View File

@ -6,7 +6,7 @@
import { exec } from "child_process";
import { promisify } from "util";
import { mkdtempSync, writeFileSync, unlinkSync, rmSync, rmdirSync } from "node:fs";
import { mkdtempSync, writeFileSync, unlinkSync, rmdirSync } from "node:fs";
import { join } from "node:path";
import type {
CodexReview,
@ -323,7 +323,7 @@ function buildCodexCommand(
}
},
};
} catch (error) {
} catch {
// ファイル書き込みエラー時はフォールバック
console.warn("[CodexReviewer] Failed to write temp file, using inline content");
}

View File

@ -3,47 +3,12 @@
*/
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
saveState,
restoreState,
getPendingSessions,
deleteSession,
updateStatus,
cleanupExpiredSessions,
initializeTable,
} from "./manager.js";
import { SessionStatus } from "./types.js";
import type { PersistedSessionState } from "./types.js";
// DynamoDBモック
vi.mock("@aws-sdk/client-dynamodb");
vi.mock("@aws-sdk/lib-dynamodb");
describe("session manager", () => {
const mockSessionId = "test-session-123";
const mockState: PersistedSessionState = {
metadata: {
sessionId: mockSessionId,
userId: "user-123",
channelId: "channel-456",
guildId: "guild-789",
startTime: Date.now(),
lastUpdateTime: Date.now(),
status: SessionStatus.RUNNING,
expiresAt: Math.floor(Date.now() / 1000) + 3600,
},
thetaState: {
runId: "run-123",
startTime: Date.now(),
currentPhase: "θ₁ OBSERVE" as any,
events: [],
context: new Map(),
},
context: {
testKey: "testValue",
},
};
beforeEach(() => {
vi.clearAllMocks();
});

View File

@ -69,7 +69,7 @@ export async function recoverPendingSessions(
// 期限切れチェック
const validSessions = sessions.filter((s) => {
if (Date.now() > s.metadata.expiresAt * 1000) {
deleteSession(s.metadata.sessionId);
void deleteSession(s.metadata.sessionId);
result.expired++;
return false;
}
@ -129,7 +129,7 @@ export async function recoverPendingSessions(
* @param ttl - TTL (), デフォルト: 1時間
*/
export async function heartbeatSession(sessionId: string, ttl: number = 3600): Promise<void> {
const expiresAt = Math.floor(Date.now() / 1000) + ttl;
const _expiresAt = Math.floor(Date.now() / 1000) + ttl;
// TODO: UpdateItemでexpiresAtを更新
// 現状のupdateStatus()はlastUpdateTimeのみ更新
@ -169,7 +169,7 @@ export async function completeSession(
* @param sessionId - ID
* @param error -
*/
export async function failSession(sessionId: string, error: Error): Promise<void> {
export async function failSession(sessionId: string, _error: Error): Promise<void> {
await updateStatus(sessionId, SessionStatus.ERROR);
// エラー情報を記録

View File

@ -4,7 +4,7 @@
* DynamoDBでθサイクルのセッション状態を永続化するための型
*/
import type { ThetaCycleState, ThetaPhase } from "../theta/types.js";
import type { ThetaCycleState } from "../theta/types.js";
/**
*

View File

@ -204,7 +204,7 @@ function withTimeout<T>(
}, timeoutMs);
// Promiseが解決したらクリアンアップキャンセルされても実行
promise.finally?.(() => clearTimeout(timeoutId));
void promise.finally?.(() => clearTimeout(timeoutId));
}),
]);
}

View File

@ -63,7 +63,10 @@ import { ThetaPhase } from "./types.js";
/**
* θ
*/
export function createThetaCycleState(runId: string, options?: ThetaCycleOptions): ThetaCycleState {
export function createThetaCycleState(
runId: string,
_options?: ThetaCycleOptions,
): ThetaCycleState {
return {
runId,
startTime: Date.now(),
@ -127,7 +130,7 @@ export async function runThetaCycle<T = unknown>(
const improveState = improveResult.state;
// サマリー生成
const summary = generateCycleSummary(improveState);
const _summary = generateCycleSummary(improveState);
// オプションのコールバック
if (options?.onEvent) {

View File

@ -15,9 +15,7 @@
* - θ (Observe Analyze Decide Allocate Execute Improve)
*/
import { describe, it, expect, beforeAll, beforeEach } from "vitest";
import { readFileSync } from "fs";
import { join } from "path";
import { describe, it, expect } from "vitest";
// Agent definitions based on Miyabi Agent Society
interface Agent {
@ -121,13 +119,6 @@ function executeAgentTask(agentId: string, task: string): ThetaCycleResult {
};
}
function validateDependencies(agentId: string): boolean {
const agent = MIYABI_AGENTS[agentId];
if (!agent) return false;
return agent.dependencies.every((dep) => MIYABI_AGENTS[dep]);
}
function validateHandoff(fromAgent: string, toAgent: string): boolean {
const from = MIYABI_AGENTS[fromAgent];
const to = MIYABI_AGENTS[toAgent];
@ -305,13 +296,6 @@ describe("Agent Flow Integration Tests", () => {
describe("Escalation (エスカレーション)", () => {
it("should escalate to しきるん on failure", () => {
const failureResult: ThetaCycleResult = {
phase: "execute",
agent: "kaede",
status: "blocked",
error: "Technical issue",
};
// Escalate to conductor
const escalation = {
from: "kaede",
@ -483,7 +467,7 @@ ${handoff.context}
// Simulate θサイクル
["observe", "analyze", "decide", "allocate", "execute", "improve"].forEach(() => {
// Simulate phase
1 + 1;
void 1;
});
const cycleEnd = Date.now();

View File

@ -8,10 +8,9 @@
* 4. PR creation/review
*/
import { describe, it, expect, beforeAll, afterAll, afterEach, vi } from "vitest";
import { describe, it, expect, beforeAll, afterAll, afterEach } from "vitest";
import { execSync } from "child_process";
import { readFileSync, unlinkSync, existsSync } from "fs";
import { join } from "path";
import { unlinkSync, existsSync } from "fs";
// Test repository configuration
const TEST_REPO = process.env.GITHUBOPS_TEST_REPO || "ShunsukeHayashi/dev-workspace";
@ -27,7 +26,7 @@ function runGhCommand(args: string): string {
});
} catch (error: unknown) {
const err = error as { stdout?: string; stderr?: string };
throw new Error(`gh command failed: ${err.stderr || err.stdout || error}`);
throw new Error(`gh command failed: ${err.stderr || err.stdout || String(error)}`);
}
}
@ -35,7 +34,7 @@ function createTestBranch(branchName: string): void {
try {
execSync(`git checkout -b ${branchName}`, { encoding: "utf-8" });
} catch (error: unknown) {
throw new Error(`Failed to create branch: ${error}`);
throw new Error(`Failed to create branch: ${String(error)}`);
}
}

View File

@ -9,8 +9,8 @@
* - Session-based agent selection
*/
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { mkdirSync, rmSync, existsSync, writeFileSync } from "fs";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { mkdirSync, rmSync, existsSync } from "fs";
import { join } from "path";
import { tmpdir } from "os";
import { randomBytes } from "crypto";

View File

@ -11,7 +11,7 @@
* - Auth profile resolution for model access
*/
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { describe, expect, it, vi } from "vitest";
// Mock dependencies
vi.mock("../../src/agents/defaults.js", () => ({
@ -42,7 +42,6 @@ import {
getModelRefStatus,
resolveAllowedModelRef,
resolveThinkingDefault,
type ModelRef,
type ThinkLevel,
} from "../../src/agents/model-selection.js";
@ -864,7 +863,7 @@ describe("Engine Unit Tests", () => {
},
});
const primary = resolveAgentModelPrimary(cfg, "main");
const _primary = resolveAgentModelPrimary(cfg, "main");
const fallbacks = resolveAgentModelFallbacksOverride(cfg, "main");
// main agent has no model override, so it inherits from defaults

View File

@ -9,7 +9,7 @@
* - Error handling and retries
*/
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
// Mock MCP tool types
interface MCPTool {

View File

@ -0,0 +1,33 @@
# AWS Terraform Variables for Clawdbot ECS Fargate Deployment
# Copy this file to terraform.tfvars and fill in the values
# AWS Configuration
aws_region = "ap-northeast-1"
aws_account_id = "123456789012" # Your AWS account ID
# Environment
environment = "production"
project_name = "clawdbot"
# ECS Configuration
ecs_task_cpu = 256 # 0.25 vCPU
ecs_task_memory = 2048 # 2 GB
ecs_service_desired_count = 1
# Discord Token (DEPRECATED - Use Secrets Manager instead)
# discord_token = "your_bot_token_here" # DO NOT use in production
# Secrets Manager Secret Name for Discord Bot Token
discord_token_secret_name = "clawdbot/discord-token"
# Create the secret in AWS Secrets Manager before running Terraform:
# aws secretsmanager create-secret \
# --name clawdbot/discord-token \
# --secret-string "your_bot_token_here"
# DynamoDB Tables
sessions_table_name = "clawdbot-sessions"
artifacts_table_name = "clawdbot-artifacts"
# S3 Bucket
artifacts_bucket_name = "clawdbot-artifacts"
artifacts_bucket_ttl_days = 7

View File

@ -0,0 +1,25 @@
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/aws" {
version = "5.100.0"
constraints = "~> 5.0"
hashes = [
"h1:Ijt7pOlB7Tr7maGQIqtsLFbl7pSMIj06TVdkoSBcYOw=",
"zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644",
"zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2",
"zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274",
"zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b",
"zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862",
"zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342",
"zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425",
"zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93",
"zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2",
"zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e",
"zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421",
"zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4",
"zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9",
"zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9",
"zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70",
]
}

View File

@ -0,0 +1,141 @@
# Clawdbot AWS Deployment
ECS Fargateを使用したClawdbotのAWSデプロイメント設定
## 前提条件
- AWS CLI設定済み
- Terraformインストール済み
- Dockerインストール済み
## セットアップ手順
### 1. Discord Bot TokenをSecrets Managerに保存
```bash
# Secrets Managerにシークレットを作成
aws secretsmanager create-secret \
--name clawdbot/discord-token \
--description "Discord Bot Token for Clawdbot" \
--secret-string "your_discord_bot_token_here"
# シークレットを確認
aws secretsmanager get-secret-value --secret-id clawdbot/discord-token
```
### 2. Terraform変数の設定
`terraform.tfvars.example`をコピーして`terraform.tfvars`を作成:
```bash
cp terraform.tfvars.example terraform.tfvars
```
`terraform.tfvars`を編集して以下の値を設定:
- `aws_account_id`: AWSアカウントID13桁の数字
- `discord_token_secret_name`: Secrets Managerのシークレット名デフォルト: `clawdbot/discord-token`
**重要**: `aws_account_id`は必須です。AWSコンソールまたは以下のコマンドで確認できます:
```bash
aws sts get-caller-identity --query Account --output text
```
### 3. Terraformの実行
```bash
# Terraform初期化
terraform init
# 設定確認
terraform plan
# デプロイ実行
terraform apply
```
### 4. Dockerイメージのプッシュ
```bash
# ECRログイン
aws ecr get-login-password --region ap-northeast-1 | \
docker login --username AWS --password-stdin <account-id>.dkr.ecr.ap-northeast-1.amazonaws.com
# イメージビルド
docker build -t clawdbot .
# タグ付け
docker tag clawdbot:latest <account-id>.dkr.ecr.ap-northeast-1.amazonaws.com/clawdbot:latest
# プッシュ
docker push <account-id>.dkr.ecr.ap-northeast-1.amazonaws.com/clawdbot:latest
```
### 5. ECSサービスの更新
```bash
# 新しいタスク定義でECSサービスを更新
aws ecs update-service \
--cluster clawdbot-cluster \
--service clawdbot-service \
--force-new-deployment
```
## セキュリティベストプラクティス
- ✅ Discord Bot TokenはAWS Secrets Managerに保存
- ✅ IAMロールで最小権限の原則を適用
- ✅ S3バケットでAES256暗号化を有効化
- ✅ DynamoDBでPITRPoint-in-Time Recoveryを有効化
- ✅ VPC内にプライベートサブネットを配置
## コスト最適化
- ECS Fargate: 256 CPU, 2048 MBメモリ最小構成
- CloudWatch Logs: 7日間保持
- S3ライフサイクル: 7日間で古いアーティファクトを自動削除
## 監視とログ
### CloudWatch Logs
```bash
# ログストリーム確認
aws logs tail /ecs/clawdbot --follow
```
### ECSタスクの状態確認
```bash
# サービス状態
aws ecs describe-services --cluster clawdbot-cluster --services clawdbot-service
# タスク一覧
aws ecs list-tasks --cluster clawdbot-cluster
```
## トラブルシューティング
### コンテナが起動しない場合
1. CloudWatch Logsでエラーを確認
2. タスク定義の環境変数を確認
3. Secrets Managerからシークレットが取得できているか確認
### Discord Botが応答しない場合
1. ECSタスクが実行中か確認
2. セキュリティグループでアウトバウンド通信が許可されているか確認
3. Discord Bot Tokenが正しいか確認
## クリーンアップ
```bash
terraform destroy
```
注意: Secrets Managerのシークレットは手動で削除する必要があります:
```bash
aws secretsmanager delete-secret --secret-id clawdbot/discord-token
```

View File

@ -0,0 +1,124 @@
/**
* DynamoDB Tables for θ-cycle Session Persistence & Artifact Storage
*/
resource "aws_dynamodb_table" "sessions" {
name = var.sessions_table_name
billing_mode = "PAY_PER_REQUEST"
hash_key = "sessionId"
attribute {
name = "sessionId"
type = "S"
}
attribute {
name = "userId"
type = "S"
}
attribute {
name = "guildId"
type = "S"
}
attribute {
name = "status"
type = "S"
}
global_secondary_index {
name = "UserIndex"
hash_key = "userId"
range_key = "status"
projection_type = "ALL"
}
global_secondary_index {
name = "GuildIndex"
hash_key = "guildId"
range_key = "status"
projection_type = "ALL"
}
point_in_time_recovery {
enabled = true
}
ttl {
attribute_name = "expiresAt"
}
tags = {
Name = "${var.project_name}-sessions"
}
}
resource "aws_dynamodb_table" "artifacts" {
name = var.artifacts_table_name
billing_mode = "PAY_PER_REQUEST"
hash_key = "id"
attribute {
name = "id"
type = "S"
}
attribute {
name = "sessionId"
type = "S"
}
attribute {
name = "userId"
type = "S"
}
attribute {
name = "type"
type = "S"
}
attribute {
name = "createdAt"
type = "N"
}
attribute {
name = "expiresAt"
type = "N"
}
global_secondary_index {
name = "SessionIndex"
hash_key = "sessionId"
range_key = "createdAt"
projection_type = "ALL"
}
global_secondary_index {
name = "UserIndex"
hash_key = "userId"
range_key = "createdAt"
projection_type = "ALL"
}
global_secondary_index {
name = "TypeIndex"
hash_key = "type"
range_key = "expiresAt"
projection_type = "ALL"
}
point_in_time_recovery {
enabled = true
}
ttl {
attribute_name = "expiresAt"
}
tags = {
Name = "${var.project_name}-artifacts"
}
}

View File

@ -0,0 +1,36 @@
/**
* Amazon ECR Repository for Clawdbot Container Images
*/
resource "aws_ecr_repository" "clawdbot" {
name = "${var.project_name}/bot"
image_tag_mutability = "MUTABLE"
image_scanning_configuration {
scan_on_push = true
}
tags = {
Name = "${var.project_name}-ecr"
}
}
# ECR Lifecycle Policy (keep last 10 images)
resource "aws_ecr_lifecycle_policy" "clawdbot" {
repository = aws_ecr_repository.clawdbot.name
policy = jsonencode({
rules = [{
rulePriority = 1
description = "Keep last 10 images"
selection = {
tagStatus = "any"
countType = "imageCountMoreThan"
countNumber = 10
}
action = {
type = "expire"
}
}]
})
}

View File

@ -0,0 +1,126 @@
/**
* ECS Fargate Configuration for Clawdbot Discord Bot
*/
# CloudWatch Log Group
resource "aws_cloudwatch_log_group" "clawdbot" {
name = "/ecs/${var.project_name}"
retention_in_days = 7
tags = {
Name = "${var.project_name}-logs"
}
}
# ECS Cluster
resource "aws_ecs_cluster" "clawdbot" {
name = "${var.project_name}-cluster"
tags = {
Name = "${var.project_name}-ecs"
}
}
# Security Group (outbound only, no inbound needed for Discord bot)
resource "aws_security_group" "clawdbot" {
name = "${var.project_name}-sg"
description = "Security group for Clawdbot ECS tasks (outbound only)"
vpc_id = aws_vpc.clawdbot.id
# No ingress rules needed - Discord bot initiates outbound WebSocket connections
# Egress (outbound) for internet access
egress {
description = "Allow all outbound traffic"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.project_name}-sg"
}
}
# ECS Task Definition
resource "aws_ecs_task_definition" "clawdbot" {
family = "${var.project_name}-task"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = var.ecs_task_cpu
memory = var.ecs_task_memory
execution_role_arn = aws_iam_role.ecs_execution_role.arn
task_role_arn = aws_iam_role.ecs_task_role.arn
container_definitions = jsonencode([
{
name = "${var.project_name}-container"
image = "${aws_ecr_repository.clawdbot.repository_url}:latest"
cpu = var.ecs_task_cpu
memory = var.ecs_task_memory
essential = true
command = ["node", "dist/entry.js", "gateway", "--allow-unconfigured"]
environment = [
{
name = "AWS_REGION"
value = var.aws_region
},
{
name = "SESSIONS_TABLE_NAME"
value = aws_dynamodb_table.sessions.name
},
{
name = "ARTIFACTS_TABLE_NAME"
value = aws_dynamodb_table.artifacts.name
},
{
name = "ARTIFACTS_S3_BUCKET"
value = aws_s3_bucket.artifacts.id
},
{
name = "DISCORD_BOT_TOKEN"
value = data.aws_secretsmanager_secret_version.discord_token.secret_string
},
{
name = "CLAWDBOT_FORCE_BUILD"
value = "0"
}
]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.clawdbot.name
"awslogs-region" = var.aws_region
"awslogs-stream-prefix" = "ecs"
"awslogs-create-group" = "true"
}
}
}
])
tags = {
Name = "${var.project_name}-taskdef"
}
}
# ECS Service
resource "aws_ecs_service" "clawdbot" {
name = "${var.project_name}-service"
cluster = aws_ecs_cluster.clawdbot.id
task_definition = aws_ecs_task_definition.clawdbot.arn
desired_count = var.ecs_service_desired_count
launch_type = "FARGATE"
network_configuration {
subnets = aws_subnet.public[*].id
security_groups = [aws_security_group.clawdbot.id]
assign_public_ip = true
}
tags = {
Name = "${var.project_name}-service"
}
}

View File

@ -0,0 +1,100 @@
/**
* IAM Roles for ECS Fargate Task Execution
*/
# Assume role policy for ECS tasks
data "aws_iam_policy_document" "ecs_assume_role" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["ecs-tasks.amazonaws.com"]
}
}
}
# ECS Task Execution Role (for ECR pull and CloudWatch logs)
resource "aws_iam_role" "ecs_execution_role" {
name = "${var.project_name}-ecs-execution-role"
assume_role_policy = data.aws_iam_policy_document.ecs_assume_role.json
}
# Attach AmazonECSTaskExecutionRolePolicy (required for Fargate)
resource "aws_iam_role_policy_attachment" "ecs_execution_role_policy" {
role = aws_iam_role.ecs_execution_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
# ECS Task Role (for application access to DynamoDB and S3)
resource "aws_iam_role" "ecs_task_role" {
name = "${var.project_name}-ecs-task-role"
assume_role_policy = data.aws_iam_policy_document.ecs_assume_role.json
}
# Task Role Policy: DynamoDB access
resource "aws_iam_role_policy" "ecs_task_dynamodb" {
name = "${var.project_name}-dynamodb-access"
role = aws_iam_role.ecs_task_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:DeleteItem",
"dynamodb:Query",
"dynamodb:Scan"
]
Resource = [
aws_dynamodb_table.sessions.arn,
"${aws_dynamodb_table.sessions.arn}/index/*",
aws_dynamodb_table.artifacts.arn,
"${aws_dynamodb_table.artifacts.arn}/index/*"
]
}
]
})
}
# Task Role Policy: S3 access
resource "aws_iam_role_policy" "ecs_task_s3" {
name = "${var.project_name}-s3-access"
role = aws_iam_role.ecs_task_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject"
]
Resource = "${aws_s3_bucket.artifacts.arn}/*"
}
]
})
}
# Task Role Policy: Secrets Manager access for Discord token
resource "aws_iam_role_policy" "ecs_task_secrets" {
name = "${var.project_name}-secrets-access"
role = aws_iam_role.ecs_task_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["secretsmanager:GetSecretValue"]
Resource = "arn:aws:secretsmanager:${var.aws_region}:${var.aws_account_id}:secret:${var.discord_token_secret_name}-??????*"
}
]
})
}

View File

@ -0,0 +1,99 @@
/**
* Deployment Outputs
*/
output "sessions_table_name" {
description = "DynamoDB table name for session storage"
value = aws_dynamodb_table.sessions.name
}
output "artifacts_table_name" {
description = "DynamoDB table name for artifacts metadata"
value = aws_dynamodb_table.artifacts.name
}
output "artifacts_bucket_name" {
description = "S3 bucket name for artifact storage"
value = aws_s3_bucket.artifacts.id
}
output "artifacts_bucket_arn" {
description = "S3 bucket ARN"
value = aws_s3_bucket.artifacts.arn
}
output "sessions_table_arn" {
description = "DynamoDB sessions table ARN"
value = aws_dynamodb_table.sessions.arn
}
output "artifacts_table_arn" {
description = "DynamoDB artifacts table ARN"
value = aws_dynamodb_table.artifacts.arn
}
output "environment_variables" {
description = "Required environment variables for clawdbot"
value = {
SESSIONS_TABLE_NAME = aws_dynamodb_table.sessions.name
ARTIFACTS_TABLE_NAME = aws_dynamodb_table.artifacts.name
ARTIFACTS_S3_BUCKET = aws_s3_bucket.artifacts.id
AWS_REGION = var.aws_region
}
}
# ECS Outputs
output "ecs_cluster_name" {
description = "ECS cluster name"
value = aws_ecs_cluster.clawdbot.name
}
output "ecs_service_name" {
description = "ECS service name"
value = aws_ecs_service.clawdbot.name
}
output "ecs_task_definition_family" {
description = "ECS task definition family"
value = aws_ecs_task_definition.clawdbot.family
}
output "ecr_repository_url" {
description = "ECR repository URL for container images"
value = aws_ecr_repository.clawdbot.repository_url
}
output "ecr_repository_name" {
description = "ECR repository name"
value = aws_ecr_repository.clawdbot.name
}
output "cloudwatch_log_group_name" {
description = "CloudWatch Log Group name"
value = aws_cloudwatch_log_group.clawdbot.name
}
output "vpc_id" {
description = "VPC ID"
value = aws_vpc.clawdbot.id
}
output "public_subnet_ids" {
description = "Public subnet IDs"
value = aws_subnet.public[*].id
}
output "security_group_id" {
description = "Security Group ID"
value = aws_security_group.clawdbot.id
}
output "ecs_task_role_arn" {
description = "ECS Task Role ARN"
value = aws_iam_role.ecs_task_role.arn
}
output "ecs_execution_role_arn" {
description = "ECS Execution Role ARN"
value = aws_iam_role.ecs_execution_role.arn
}

View File

@ -0,0 +1,60 @@
/**
* S3 Bucket for Artifact Storage
*/
resource "aws_s3_bucket" "artifacts" {
bucket = var.artifacts_bucket_name
tags = {
Name = "${var.project_name}-artifacts"
}
}
# S3 bucket versioning (disabled for cost optimization)
resource "aws_s3_bucket_versioning" "artifacts" {
bucket = aws_s3_bucket.artifacts.bucket
versioning_configuration {
status = "Suspended"
}
}
# S3 bucket server-side encryption (SSE-S3)
resource "aws_s3_bucket_server_side_encryption_configuration" "artifacts" {
bucket = aws_s3_bucket.artifacts.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
# S3 bucket public access block (private bucket)
resource "aws_s3_bucket_public_access_block" "artifacts" {
bucket = aws_s3_bucket.artifacts.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
# S3 bucket lifecycle configuration (auto-expire old artifacts)
resource "aws_s3_bucket_lifecycle_configuration" "artifacts" {
bucket = aws_s3_bucket.artifacts.id
rule {
id = "expire-old-artifacts"
status = "Enabled"
filter {}
expiration {
days = var.artifacts_bucket_ttl_days
}
noncurrent_version_expiration {
noncurrent_days = 1
}
}
}

View File

@ -0,0 +1,8 @@
/**
* AWS Secrets Manager Data Sources
*/
# Discord Bot Token from Secrets Manager
data "aws_secretsmanager_secret_version" "discord_token" {
secret_id = var.discord_token_secret_name
}

View File

@ -0,0 +1,34 @@
# AWS Terraform Variables for Clawdbot ECS Fargate Deployment
# Copy this file to terraform.tfvars and fill in the values
# AWS Configuration
aws_region = "ap-northeast-1"
aws_account_id = "123456789012" # Your AWS account ID
# Environment
environment = "production"
project_name = "clawdbot"
# ECS Configuration
ecs_task_cpu = 256 # 0.25 vCPU
ecs_task_memory = 2048 # 2 GB
ecs_service_desired_count = 1
# Discord Token (DEPRECATED - Use Secrets Manager instead)
# discord_token = "your_bot_token_here" # DO NOT use in production
# Secrets Manager Secret Name for Discord Bot Token
discord_token_secret_name = "clawdbot/discord-token"
# Create the secret in AWS Secrets Manager before running Terraform:
# aws secretsmanager create-secret \
# --name clawdbot/discord-token \
# --description "Discord Bot Token for Clawdbot" \
# --secret-string "your_bot_token_here"
# DynamoDB Tables
sessions_table_name = "clawdbot-sessions"
artifacts_table_name = "clawdbot-artifacts"
# S3 Bucket
artifacts_bucket_name = "clawdbot-artifacts"
artifacts_bucket_ttl_days = 7

View File

@ -0,0 +1,79 @@
variable "aws_region" {
description = "AWS region for resources"
type = string
default = "ap-northeast-1"
}
variable "aws_account_id" {
description = "AWS account ID (for Secrets Manager ARN)"
type = string
}
variable "environment" {
description = "Environment name (e.g., production, staging)"
type = string
default = "production"
}
variable "project_name" {
description = "Project name used for resource naming"
type = string
default = "clawdbot"
}
variable "sessions_table_name" {
description = "DynamoDB table name for session storage"
type = string
default = "clawdbot-sessions"
}
variable "artifacts_table_name" {
description = "DynamoDB table name for artifacts metadata"
type = string
default = "clawdbot-artifacts"
}
variable "artifacts_bucket_name" {
description = "S3 bucket name for artifact storage"
type = string
default = "clawdbot-artifacts"
}
variable "artifacts_bucket_ttl_days" {
description = "S3 bucket lifecycle TTL in days"
type = number
default = 7
}
# ECS Fargate Configuration
variable "ecs_task_cpu" {
description = "ECS task CPU units (256 = 0.25 vCPU)"
type = number
default = 256
}
variable "ecs_task_memory" {
description = "ECS task memory in MB"
type = number
default = 2048
}
variable "ecs_service_desired_count" {
description = "Desired number of ECS tasks"
type = number
default = 1
}
variable "discord_token_secret_name" {
description = "AWS Secrets Manager secret name for Discord bot token"
type = string
default = "clawdbot/discord-token"
}
# DEPRECATED: Use discord_token_secret_name with AWS Secrets Manager instead
variable "discord_token" {
description = "DEPRECATED: Discord bot token (use Secrets Manager instead)"
type = string
sensitive = true
default = null
}

View File

@ -0,0 +1,23 @@
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Project = var.project_name
Environment = var.environment
ManagedBy = "Terraform"
Component = "clawdbot-backend"
}
}
}

View File

@ -0,0 +1,63 @@
/**
* VPC for Clawdbot ECS Fargate Deployment
* Discord bot requires outbound internet access for WebSocket connections
*/
# VPC (using 100.64.0.0/16 to avoid conflicts with default 10.0.0.0/8)
resource "aws_vpc" "clawdbot" {
cidr_block = "100.64.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.project_name}-vpc"
}
}
# Internet Gateway for outbound internet access
resource "aws_internet_gateway" "clawdbot" {
vpc_id = aws_vpc.clawdbot.id
tags = {
Name = "${var.project_name}-igw"
}
}
# Public Subnets (for Fargate tasks with public IP)
resource "aws_subnet" "public" {
count = 2
vpc_id = aws_vpc.clawdbot.id
cidr_block = cidrsubnet(aws_vpc.clawdbot.cidr_block, 8, count.index)
availability_zone = data.aws_availability_zones.available.names[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.project_name}-public-${count.index + 1}"
}
}
# Route Table for public subnets
resource "aws_route_table" "public" {
vpc_id = aws_vpc.clawdbot.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.clawdbot.id
}
tags = {
Name = "${var.project_name}-public-rt"
}
}
# Route Table Association for public subnets
resource "aws_route_table_association" "public" {
count = 2
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
# Data source for available AZs
data "aws_availability_zones" "available" {
state = "available"
}