Development Guide
Project Structure
Section titled “Project Structure”chukfi-teams-bot/├── main.py # FastAPI server, routes, HMAC verification├── agent.py # Bedrock Converse API, tool definitions, tool loop├── executor.py # Guarded CLI subprocess wrapper (safety gate)├── config.py # Hardcoded guardrails + env-based settings├── models.py # Pydantic models (Teams payload, responses)├── requirements.txt # Python dependencies├── Dockerfile # Multi-stage Docker build├── .env.example # Environment variable template└── README.md # Quick start guideAdding a New Tool
Section titled “Adding a New Tool”1. Add the executor function
Section titled “1. Add the executor function”In executor.py, add a new handler function:
def handle_delete_entry(entry_id: str) -> dict: """Delete a content entry.""" args = [ "content", "delete", "--id", entry_id, "--json", ] return _run_cli(args)2. Add the tool definition
Section titled “2. Add the tool definition”In agent.py, add the tool spec to the TOOLS list:
{ "toolSpec": { "name": "delete_entry", "description": "Delete a content entry by its UUID. " "Use this when staff asks to remove content.", "inputSchema": { "json": { "type": "object", "properties": { "id": { "type": "string", "description": "Entry UUID to delete", }, }, "required": ["id"], } }, }},3. Wire up the handler
Section titled “3. Wire up the handler”In agent.py, add the tool call handler in _handle_tool_call():
elif tool_name == "delete_entry": result = handle_delete_entry( entry_id=tool_input["id"], )4. Add guardrails (if needed)
Section titled “4. Add guardrails (if needed)”If the new tool needs safety controls, add them in config.py:
# Prevent deletion of published contentALLOW_DELETE_PUBLISHED: ClassVar[bool] = FalseTesting
Section titled “Testing”Unit Tests
Section titled “Unit Tests”Test the executor functions directly (they don’t need Bedrock):
from executor import handle_create_entry, GuardrailViolation
def test_create_entry_rejects_unknown_type(): with pytest.raises(GuardrailViolation): handle_create_entry(type_slug="malicious-type", title="test")
def test_create_entry_always_draft(): result = handle_create_entry(type_slug="blog-posts", title="Test") assert result["success"]Integration Tests
Section titled “Integration Tests”Test the full pipeline with a local Bedrock endpoint:
# Start the serverTEAMS_WEBHOOK_SECRET="" uvicorn main:app --reload --port 8000
# Test createcurl -X POST http://localhost:8000/api/webhook \ -H "Content-Type: application/json" \
# Test listcurl -X POST http://localhost:8000/api/webhook \ -H "Content-Type: application/json" \Debugging
Section titled “Debugging”Enable Debug Logging
Section titled “Enable Debug Logging”LOG_LEVEL=DEBUG uvicorn main:app --reloadCommon Issues
Section titled “Common Issues”| Symptom | Likely Cause | Fix |
|---|---|---|
401 Invalid HMAC signature |
Wrong webhook secret | Check TEAMS_WEBHOOK_SECRET matches Teams config |
Bedrock API error |
No Bedrock access | Verify IAM permissions and model access in AWS console |
CLI failed (exit=1) |
Database unreachable | Check DATABASE_URL and PostgreSQL connectivity |
Access Denied |
Wrong tenant | Update ALLOWED_TENANTS in config.py |
| Bot not responding in Teams | Wrong callback URL | Verify the webhook URL is publicly accessible |
Testing HMAC Locally
Section titled “Testing HMAC Locally”import base64, hashlib, hmac
secret = "your-teams-secret"body = b'{"text":"test"}'
key = base64.b64decode(secret)hashed = hmac.new(key, body, hashlib.sha256).digest()sig = base64.b64encode(hashed).decode("utf-8")
print(f"Authorization: HMAC {sig}")Security Best Practices
Section titled “Security Best Practices”- Never put secrets in code — Use environment variables or AWS Secrets Manager
- Keep guardrails in Python, not prompts — Prompt-based guardrails can be bypassed
- Use least-privilege IAM — The bot only needs
bedrock:InvokeModelfor one model - Enable HMAC in production — Dev mode (
TEAMS_WEBHOOK_SECRET="") is for local testing only - Monitor for unauthorized access — Set up alerts on 401 responses
- Keep Claude’s temperature low — 0.3 ensures deterministic tool selection
- Limit tool rounds — 5 max prevents runaway loops