diff options
Diffstat (limited to 'AI_AGENT_ACCESS.md')
| -rw-r--r-- | AI_AGENT_ACCESS.md | 340 |
1 files changed, 340 insertions, 0 deletions
diff --git a/AI_AGENT_ACCESS.md b/AI_AGENT_ACCESS.md new file mode 100644 index 0000000..40fe6a1 --- /dev/null +++ b/AI_AGENT_ACCESS.md @@ -0,0 +1,340 @@ +# AI Agent Access Design + +## Overview +Add a dedicated API endpoint optimized for AI agents (like Claude) to access dashboard data in a structured, machine-readable format with simple authentication. + +## Authentication Strategy + +### API Key-Based Auth +Simple bearer token authentication that's easy to use from Claude.ai: +- Generate a long, random API key for AI access +- Store in environment variable: `AI_AGENT_API_KEY` +- Use standard `Authorization: Bearer {key}` header +- No complex OAuth flows or JWT rotation + +### Why This Approach? +1. **Simple for Claude**: Just include auth header in requests +2. **Stateless**: No session management needed +3. **Revocable**: Change key in .env to revoke access +4. **Standard**: Uses common HTTP authorization pattern + +## API Endpoints + +### GET /api/ai/dashboard +Returns complete dashboard data in AI-optimized JSON format. + +**Request:** +```http +GET /api/ai/dashboard HTTP/1.1 +Host: localhost:8080 +Authorization: Bearer {AI_AGENT_API_KEY} +``` + +**Response (Success - 200 OK):** +```json +{ + "status": "success", + "timestamp": "2026-01-09T14:30:00Z", + "data": { + "trello_boards": [ + { + "id": "board123", + "name": "Work Projects", + "cards": [ + { + "id": "card1", + "name": "Complete project proposal", + "list": "In Progress", + "due_date": "2026-01-15", + "url": "https://trello.com/c/card1" + } + ] + } + ], + "todoist_tasks": [ + { + "id": "task1", + "content": "Buy groceries", + "project": "Personal", + "priority": 1, + "due_date": "2026-01-10", + "completed": false, + "labels": ["shopping"], + "url": "https://todoist.com/task/1" + } + ], + "obsidian_notes": [ + { + "filename": "meeting-notes.md", + "title": "Team Meeting Notes", + "content_preview": "Discussed Q1 goals...", + "modified": "2026-01-09T10:00:00Z", + "tags": ["meetings", "work"] + } + ], + "plantoeat_meals": [ + { + "id": "meal1", + "recipe": "Spaghetti Carbonara", + "date": "2026-01-09", + "meal_type": "dinner", + "url": "https://plantoeat.com/recipe/123" + } + ] + }, + "metadata": { + "cache_age_seconds": 45, + "sources_refreshed": ["trello", "todoist"], + "errors": [] + } +} +``` + +**Response (Unauthorized - 401):** +```json +{ + "status": "error", + "error": "Invalid or missing API key", + "hint": "Include 'Authorization: Bearer {key}' header" +} +``` + +### GET /api/ai/boards/{board_id}/cards +Get cards from a specific Trello board. + +**Request:** +```http +GET /api/ai/boards/board123/cards HTTP/1.1 +Authorization: Bearer {AI_AGENT_API_KEY} +``` + +### GET /api/ai/tasks?filter={filter} +Get filtered tasks (today, week, overdue, completed). + +**Request:** +```http +GET /api/ai/tasks?filter=today HTTP/1.1 +Authorization: Bearer {AI_AGENT_API_KEY} +``` + +### POST /api/ai/task +Create a new Todoist task (Phase 2). + +**Request:** +```http +POST /api/ai/task HTTP/1.1 +Authorization: Bearer {AI_AGENT_API_KEY} +Content-Type: application/json + +{ + "content": "Buy milk", + "project": "Personal", + "due_date": "2026-01-10", + "priority": 1, + "labels": ["shopping"] +} +``` + +## AI-Optimized Response Format + +### Design Principles +1. **Flat structure**: Avoid deep nesting where possible +2. **Explicit field names**: Use `due_date` not `dueDate` for clarity +3. **Include metadata**: Provide context about data freshness +4. **Clear errors**: Return actionable error messages +5. **Consistent types**: Always use ISO 8601 for dates + +### Example Claude.ai Usage +```markdown +User: "What tasks do I have due today?" + +Claude uses MCP tool or API call: +GET /api/ai/tasks?filter=today +Authorization: Bearer sk-ai-1234567890abcdef + +Claude response: "You have 3 tasks due today: +1. Buy groceries (Personal, Priority 1) +2. Complete project proposal (Work, Priority 2) +3. Call dentist (Health, Priority 3)" +``` + +## Implementation + +### Middleware: Auth Check +```go +// internal/middleware/ai_auth.go +func AIAuthMiddleware(apiKey string) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + authHeader := r.Header.Get("Authorization") + + if !strings.HasPrefix(authHeader, "Bearer ") { + respondJSON(w, 401, map[string]string{ + "status": "error", + "error": "Missing or invalid Authorization header", + "hint": "Include 'Authorization: Bearer {key}' header", + }) + return + } + + token := strings.TrimPrefix(authHeader, "Bearer ") + if token != apiKey { + respondJSON(w, 401, map[string]string{ + "status": "error", + "error": "Invalid API key", + }) + return + } + + next.ServeHTTP(w, r) + }) + } +} +``` + +### Handler: AI Dashboard +```go +// internal/handlers/ai_handlers.go +func (h *Handler) HandleAIDashboard(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + // Fetch all data + data, err := h.fetchDashboardData(ctx, false) + if err != nil { + respondJSON(w, 500, map[string]interface{}{ + "status": "error", + "error": err.Error(), + }) + return + } + + // Format for AI consumption + response := map[string]interface{}{ + "status": "success", + "timestamp": time.Now().Format(time.RFC3339), + "data": map[string]interface{}{ + "trello_boards": formatBoardsForAI(data.Boards), + "todoist_tasks": formatTasksForAI(data.Tasks), + "obsidian_notes": formatNotesForAI(data.Notes), + "plantoeat_meals": formatMealsForAI(data.Meals), + }, + "metadata": map[string]interface{}{ + "errors": data.Errors, + }, + } + + respondJSON(w, 200, response) +} +``` + +### Router Setup +```go +// cmd/dashboard/main.go +func main() { + // ... existing setup ... + + // AI Agent routes (protected by auth middleware) + if cfg.AIAgentAPIKey != "" { + aiRouter := chi.NewRouter() + aiRouter.Use(middleware.AIAuthMiddleware(cfg.AIAgentAPIKey)) + + r.Route("/api/ai", func(r chi.Router) { + r.Use(middleware.AIAuthMiddleware(cfg.AIAgentAPIKey)) + r.Get("/dashboard", h.HandleAIDashboard) + r.Get("/boards/{boardID}/cards", h.HandleAIBoardCards) + r.Get("/tasks", h.HandleAITasks) + // Phase 2: Write operations + // r.Post("/task", h.HandleAICreateTask) + }) + } +} +``` + +## Security Considerations + +1. **Key Storage**: Never commit API key to git +2. **Key Rotation**: Easy to change in .env file +3. **Rate Limiting**: Consider adding rate limits per key +4. **HTTPS Only**: Use HTTPS in production +5. **Logging**: Log API key usage (last 4 chars only) + +## Configuration + +### .env.example +```bash +# AI Agent Access +AI_AGENT_API_KEY=sk-ai-1234567890abcdef... +# Generate with: openssl rand -hex 32 +``` + +### config.go +```go +type Config struct { + // ... existing fields ... + AIAgentAPIKey string +} + +func Load() (*Config, error) { + cfg := &Config{ + // ... existing fields ... + AIAgentAPIKey: os.Getenv("AI_AGENT_API_KEY"), + } + return cfg, nil +} +``` + +## Usage from Claude.ai + +### With MCP (Model Context Protocol) +```json +{ + "mcpServers": { + "dashboard": { + "command": "curl", + "args": [ + "-H", "Authorization: Bearer ${AI_AGENT_API_KEY}", + "http://localhost:8080/api/ai/dashboard" + ] + } + } +} +``` + +### Direct API Call +Claude can make HTTP requests directly: +``` +GET http://localhost:8080/api/ai/dashboard +Authorization: Bearer sk-ai-1234567890abcdef +``` + +## Future Enhancements + +1. **Webhook Support**: POST updates to Claude.ai when data changes +2. **Streaming**: Server-sent events for real-time updates +3. **Scoped Keys**: Different keys for read vs write access +4. **GraphQL**: More flexible querying for AI agents +5. **Natural Language Queries**: `/api/ai/query?q=What's due today?` + +## Testing + +### Manual Test +```bash +# Generate API key +export AI_KEY=$(openssl rand -hex 32) + +# Add to .env +echo "AI_AGENT_API_KEY=sk-ai-$AI_KEY" >> .env + +# Test endpoint +curl -H "Authorization: Bearer sk-ai-$AI_KEY" \ + http://localhost:8080/api/ai/dashboard | jq . +``` + +### Unit Test +```go +func TestAIAuthMiddleware(t *testing.T) { + // Test valid key + // Test invalid key + // Test missing header +} +``` |
