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:
parent
3c63ee1d6d
commit
4d74fdf593
93
.claude/DISCORD_NOTIFY.md
Normal file
93
.claude/DISCORD_NOTIFY.md
Normal 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
170
.claude/SECOND_BRAIN.md
Normal 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
43
.claude/SESSION_NOTIFY.md
Normal 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
86
.claude/TASK_TRACKING.md
Normal 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 セッションから通知が届きます。
|
||||
178
.claude/hooks/discord-brain-save.sh
Executable file
178
.claude/hooks/discord-brain-save.sh
Executable 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
|
||||
96
.claude/hooks/discord-task-end.sh
Executable file
96
.claude/hooks/discord-task-end.sh
Executable 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}"
|
||||
|
||||
# セッションID(PPIDを使用してサブシェルでも一貫性を確保)
|
||||
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
|
||||
64
.claude/hooks/discord-task-start.sh
Executable file
64
.claude/hooks/discord-task-start.sh
Executable 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}"
|
||||
|
||||
# セッションID(PPIDを使用してサブシェルでも一貫性を確保)
|
||||
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
32
.claude/settings.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
13
.env.example
13
.env.example
@ -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
8
.gitignore
vendored
@ -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
|
||||
|
||||
@ -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"]
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
76
scripts/list-discord-channels.ts
Normal file
76
scripts/list-discord-channels.ts
Normal 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();
|
||||
@ -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();
|
||||
});
|
||||
|
||||
@ -25,7 +25,6 @@ import type {
|
||||
ArtifactType,
|
||||
SaveArtifactOptions,
|
||||
DownloadUrlOptions,
|
||||
ArtifactFilter,
|
||||
} from "./types.js";
|
||||
|
||||
/** デフォルトTTL (24時間) */
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
@ -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リプライオプション拡張
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
runCodexReview,
|
||||
parseCodexOutput,
|
||||
createReviewRequest,
|
||||
evaluateReview,
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
// エラー情報を記録
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
* DynamoDBでθサイクルのセッション状態を永続化するための型
|
||||
*/
|
||||
|
||||
import type { ThetaCycleState, ThetaPhase } from "../theta/types.js";
|
||||
import type { ThetaCycleState } from "../theta/types.js";
|
||||
|
||||
/**
|
||||
* セッションの状態
|
||||
|
||||
@ -204,7 +204,7 @@ function withTimeout<T>(
|
||||
}, timeoutMs);
|
||||
|
||||
// Promiseが解決したらクリアンアップ(キャンセルされても実行)
|
||||
promise.finally?.(() => clearTimeout(timeoutId));
|
||||
void promise.finally?.(() => clearTimeout(timeoutId));
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
33
workers/clawdbot-aws/.env.example
Normal file
33
workers/clawdbot-aws/.env.example
Normal 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
|
||||
25
workers/clawdbot-aws/terraform/.terraform.lock.hcl
generated
Normal file
25
workers/clawdbot-aws/terraform/.terraform.lock.hcl
generated
Normal 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",
|
||||
]
|
||||
}
|
||||
141
workers/clawdbot-aws/terraform/README.md
Normal file
141
workers/clawdbot-aws/terraform/README.md
Normal 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アカウントID(13桁の数字)
|
||||
- `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でPITR(Point-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
|
||||
```
|
||||
124
workers/clawdbot-aws/terraform/dynamodb.tf
Normal file
124
workers/clawdbot-aws/terraform/dynamodb.tf
Normal 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"
|
||||
}
|
||||
}
|
||||
36
workers/clawdbot-aws/terraform/ecr.tf
Normal file
36
workers/clawdbot-aws/terraform/ecr.tf
Normal 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"
|
||||
}
|
||||
}]
|
||||
})
|
||||
}
|
||||
126
workers/clawdbot-aws/terraform/ecs.tf
Normal file
126
workers/clawdbot-aws/terraform/ecs.tf
Normal 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"
|
||||
}
|
||||
}
|
||||
100
workers/clawdbot-aws/terraform/iam.tf
Normal file
100
workers/clawdbot-aws/terraform/iam.tf
Normal 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}-??????*"
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
99
workers/clawdbot-aws/terraform/outputs.tf
Normal file
99
workers/clawdbot-aws/terraform/outputs.tf
Normal 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
|
||||
}
|
||||
60
workers/clawdbot-aws/terraform/s3.tf
Normal file
60
workers/clawdbot-aws/terraform/s3.tf
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
8
workers/clawdbot-aws/terraform/secrets.tf
Normal file
8
workers/clawdbot-aws/terraform/secrets.tf
Normal 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
|
||||
}
|
||||
34
workers/clawdbot-aws/terraform/terraform.tfvars.example
Normal file
34
workers/clawdbot-aws/terraform/terraform.tfvars.example
Normal 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
|
||||
79
workers/clawdbot-aws/terraform/variables.tf
Normal file
79
workers/clawdbot-aws/terraform/variables.tf
Normal 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
|
||||
}
|
||||
23
workers/clawdbot-aws/terraform/versions.tf
Normal file
23
workers/clawdbot-aws/terraform/versions.tf
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
63
workers/clawdbot-aws/terraform/vpc.tf
Normal file
63
workers/clawdbot-aws/terraform/vpc.tf
Normal 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"
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user