diff --git a/.env.example b/.env.example index 65160b8d..a58439f6 100644 --- a/.env.example +++ b/.env.example @@ -41,6 +41,15 @@ # Lightning backend: "mock" (default) | "lnd" # LIGHTNING_BACKEND=mock +# ── Environment & Privacy ─────────────────────────────────────────────────── +# Environment mode: "development" (default) | "production" +# In production, security secrets MUST be set or the app will refuse to start. +# TIMMY_ENV=development + +# Disable Agno telemetry for sovereign/air-gapped deployments. +# Default is false (disabled) to align with local-first AI vision. +# TELEMETRY_ENABLED=false + # ── Telegram bot ────────────────────────────────────────────────────────────── # Bot token from @BotFather on Telegram. # Alternatively, configure via the /telegram/setup dashboard endpoint at runtime. diff --git a/.gitignore b/.gitignore index 529a9379..44235108 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,9 @@ env/ # SQLite memory — never commit agent memory *.db +# Runtime PID files +.watchdog.pid + # Chat platform state files (contain bot tokens) telegram_state.json discord_state.json @@ -37,5 +40,11 @@ reports/ .vscode/ *.swp *.swo -.DS_Store .claude/ + +# macOS +.DS_Store +.AppleDouble +.LSOverride +.Spotlight-V100 +.Trashes diff --git a/Dockerfile b/Dockerfile index 7efebd2c..6ac6daa6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,29 +21,22 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ WORKDIR /app # ── Python deps (install before copying src for layer caching) ─────────────── +# Copy only pyproject.toml first so Docker can cache the dep-install layer. +# The editable install (-e) happens after src is copied below. COPY pyproject.toml . -# Install production deps only (no dev/test extras in the image) -RUN pip install --no-cache-dir \ - "fastapi>=0.115.0" \ - "uvicorn[standard]>=0.32.0" \ - "jinja2>=3.1.0" \ - "httpx>=0.27.0" \ - "python-multipart>=0.0.12" \ - "aiofiles>=24.0.0" \ - "typer>=0.12.0" \ - "rich>=13.0.0" \ - "pydantic-settings>=2.0.0" \ - "websockets>=12.0" \ - "agno[sqlite]>=1.4.0" \ - "ollama>=0.3.0" \ - "openai>=1.0.0" \ - "python-telegram-bot>=21.0" \ - "GitPython>=3.1.40" \ - "moviepy>=2.0.0" \ - "redis>=5.0.0" +# Create a minimal src layout so `pip install` can resolve the package metadata +# without copying the full source tree (preserves Docker layer caching). +RUN mkdir -p src/timmy src/timmy_serve src/self_tdd src/dashboard && \ + touch src/timmy/__init__.py src/timmy/cli.py \ + src/timmy_serve/__init__.py src/timmy_serve/cli.py \ + src/self_tdd/__init__.py src/self_tdd/watchdog.py \ + src/dashboard/__init__.py src/config.py + +RUN pip install --no-cache-dir -e ".[swarm,telegram]" # ── Application source ─────────────────────────────────────────────────────── +# Overwrite the stubs with real source code COPY src/ ./src/ COPY static/ ./static/ diff --git a/Makefile b/Makefile index 97d5a21b..5f6a2f9e 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: install install-bigbrain dev nuke test test-cov test-cov-html watch lint clean help \ +.PHONY: install install-bigbrain install-creative dev nuke test test-cov test-cov-html watch lint clean help \ up down logs \ docker-build docker-up docker-down docker-agent docker-logs docker-shell \ cloud-deploy cloud-up cloud-down cloud-logs cloud-status cloud-update @@ -25,13 +25,24 @@ install-bigbrain: $(VENV)/bin/activate echo "✓ AirLLM installed (PyTorch backend)"; \ fi +install-creative: $(VENV)/bin/activate + $(PIP) install --quiet -e ".[dev,creative]" + @if [ "$$(uname -m)" = "arm64" ] && [ "$$(uname -s)" = "Darwin" ]; then \ + echo " Apple Silicon detected — installing PyTorch with Metal (MPS) support..."; \ + $(PIP) install --quiet --pre torch torchvision torchaudio \ + --index-url https://download.pytorch.org/whl/nightly/cpu; \ + echo "✓ Creative extras installed with Metal GPU acceleration"; \ + else \ + echo "✓ Creative extras installed (diffusers, torch, ace-step)"; \ + fi + $(VENV)/bin/activate: python3 -m venv $(VENV) # ── Development ─────────────────────────────────────────────────────────────── dev: nuke - $(UVICORN) dashboard.app:app --reload --host 0.0.0.0 --port 8000 + PYTHONDONTWRITEBYTECODE=1 $(UVICORN) dashboard.app:app --reload --host 0.0.0.0 --port 8000 # Kill anything on port 8000, stop Docker containers, clear stale state. # Safe to run anytime — idempotent, never errors out. @@ -41,9 +52,12 @@ nuke: @docker compose down --remove-orphans 2>/dev/null || true @# Kill any process holding port 8000 (errno 48 fix) @lsof -ti :8000 | xargs kill -9 2>/dev/null || true + @# Purge stale bytecache to prevent loading old .pyc files + @find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true + @find . -name "*.pyc" -delete 2>/dev/null || true @# Brief pause to let the OS release the socket @sleep 0.5 - @echo " ✓ Port 8000 free, containers stopped" + @echo " ✓ Port 8000 free, containers stopped, caches cleared" # Print the local IP addresses your phone can use to reach this machine. # Connect your phone to the same hotspot your Mac is sharing from, @@ -54,10 +68,15 @@ ip: @echo "" @echo " Open one of these on your phone: http://:8000" @echo "" - @ipconfig getifaddr en0 2>/dev/null | awk '{print " en0 (Wi-Fi): http://" $$1 ":8000"}' || true - @ipconfig getifaddr en1 2>/dev/null | awk '{print " en1 (Ethernet): http://" $$1 ":8000"}' || true - @ipconfig getifaddr en2 2>/dev/null | awk '{print " en2: http://" $$1 ":8000"}' || true - @ifconfig 2>/dev/null | awk '/inet / && !/127\.0\.0\.1/ && !/::1/{print " " $$2 " → http://" $$2 ":8000"}' | head -5 || true + @if [ "$$(uname -s)" = "Darwin" ]; then \ + ipconfig getifaddr en0 2>/dev/null | awk '{print " en0 (Wi-Fi): http://" $$1 ":8000"}' || true; \ + ipconfig getifaddr en1 2>/dev/null | awk '{print " en1 (Ethernet): http://" $$1 ":8000"}' || true; \ + ipconfig getifaddr en2 2>/dev/null | awk '{print " en2: http://" $$1 ":8000"}' || true; \ + fi + @# Generic fallback — works on both macOS and Linux + @ifconfig 2>/dev/null | awk '/inet / && !/127\.0\.0\.1/ && !/::1/{print " " $$2 " → http://" $$2 ":8000"}' | head -5 \ + || ip -4 addr show 2>/dev/null | awk '/inet / && !/127\.0\.0\.1/{split($$2,a,"/"); print " " a[1] " → http://" a[1] ":8000"}' | head -5 \ + || true @echo "" watch: @@ -202,6 +221,7 @@ help: @echo " ─────────────────────────────────────────────────" @echo " make install create venv + install dev deps" @echo " make install-bigbrain install with AirLLM (big-model backend)" + @echo " make install-creative install with creative extras (torch, diffusers)" @echo " make dev clean up + start dashboard (auto-fixes errno 48)" @echo " make nuke kill port 8000, stop containers, reset state" @echo " make ip print local IP addresses for phone testing" diff --git a/QUALITY_REVIEW_REPORT.md b/QUALITY_REVIEW_REPORT.md new file mode 100644 index 00000000..8092e533 --- /dev/null +++ b/QUALITY_REVIEW_REPORT.md @@ -0,0 +1,232 @@ +# Timmy Time — Comprehensive Quality Review Report +**Date:** 2026-02-25 +**Reviewed by:** Claude Code +**Test Coverage:** 84.15% (895 tests passing) +**Test Result:** ✅ 895 passed, 30 skipped + +--- + +## Executive Summary + +The Timmy Time application is a **functional local-first AI agent system** with a working FastAPI dashboard, Ollama integration, and sophisticated Spark Intelligence engine. The codebase is well-structured with good test coverage, but **critical bugs were found and fixed** during this review that prevented the agent from working properly. + +**Overall Quality Score: 7.5/10** +- Architecture: 8/10 +- Functionality: 8/10 (after fixes) +- Test Coverage: 8/10 +- Documentation: 7/10 +- Memory/Self-Awareness: 9/10 + +--- + +## 1. Critical Bugs Found & Fixed + +### Bug 1: Toolkit API Mismatch (`CRITICAL`) +**Location:** `src/timmy/tools.py` +**Issue:** Code used non-existent `Toolkit.add_tool()` method (should be `register()`) + +**Changes Made:** +- Changed `toolkit.add_tool(...)` → `toolkit.register(...)` (29 occurrences) +- Changed `python_tools.python` → `python_tools.run_python_code` (3 occurrences) +- Changed `file_tools.write_file` → `file_tools.save_file` (4 occurrences) +- Changed `FileTools(base_dir=str(base_path))` → `FileTools(base_dir=base_path)` (5 occurrences) + +**Impact:** Without this fix, Timmy agent would crash on startup with `AttributeError`. + +### Bug 2: Agent Tools Parameter (`CRITICAL`) +**Location:** `src/timmy/agent.py` +**Issue:** Tools passed as single Toolkit instead of list + +**Change Made:** +- Changed `tools=tools` → `tools=[tools] if tools else None` + +**Impact:** Without this fix, Agno Agent initialization would fail with `TypeError: 'Toolkit' object is not iterable`. + +--- + +## 2. Model Inference — ✅ WORKING + +### Test Results + +| Test | Status | Details | +|------|--------|---------| +| Agent creation | ✅ Pass | Ollama backend initializes correctly | +| Basic inference | ✅ Pass | Response type: `RunOutput` with content | +| Tool usage | ✅ Pass | File operations, shell commands work | +| Streaming | ✅ Pass | Supported via `stream=True` | + +### Inference Example +``` +Input: "What is your name and who are you?" +Output: "I am Timmy, a sovereign AI agent running locally on Apple Silicon. + I'm committed to your digital sovereignty and powered by Bitcoin economics..." +``` + +### Available Models +- **Ollama:** llama3.2 (default), deepseek-r1:1.5b +- **AirLLM:** 8B, 70B, 405B models (optional backend) + +--- + +## 3. Memory & Self-Awareness — ✅ WORKING + +### Conversation Memory Test + +| Test | Status | Result | +|------|--------|--------| +| Single-turn memory | ✅ Pass | Timmy remembers what user just asked | +| Multi-turn context | ✅ Pass | References earlier conversation | +| Self-identification | ✅ Pass | "I am Timmy, a sovereign AI agent..." | +| Persistent storage | ✅ Pass | SQLite (`timmy.db`) persists across restarts | +| History recall | ✅ Pass | Can recall first question from conversation | + +### Memory Implementation +- **Storage:** SQLite via `SqliteDb` (Agno) +- **Context window:** 10 history runs (`num_history_runs=10`) +- **File:** `timmy.db` in project root + +### Self-Awareness Features +✅ Agent knows its name ("Timmy") +✅ Agent knows it's a sovereign AI +✅ Agent knows it runs locally (Apple Silicon detection) +✅ Agent references Bitcoin economics and digital sovereignty +✅ Agent references Christian faith grounding (per system prompt) + +--- + +## 4. Spark Intelligence Engine — ✅ WORKING + +### Capabilities Verified + +| Feature | Status | Details | +|---------|--------|---------| +| Event capture | ✅ Working | 550 events captured | +| Task predictions | ✅ Working | 235 predictions, 85% avg accuracy | +| Memory consolidation | ✅ Working | 6 memories stored | +| Advisories | ✅ Working | Failure prevention, performance, bid optimization | +| EIDOS loop | ✅ Working | Predict → Observe → Evaluate → Learn | + +### Sample Advisory Output +``` +[failure_prevention] Agent fail-lea has 7 failures (Priority: 1.0) +[agent_performance] Agent success- excels (100% success) (Priority: 0.6) +[bid_optimization] Wide bid spread (20–94 sats) (Priority: 0.5) +[system_health] Strong prediction accuracy (85%) (Priority: 0.3) +``` + +--- + +## 5. Dashboard & UI — ✅ WORKING + +### Route Testing Results + +| Route | Status | Notes | +|-------|--------|-------| +| `/` | ✅ 200 | Main dashboard loads | +| `/health` | ✅ 200 | Health panel | +| `/agents` | ✅ 200 | Agent list API | +| `/swarm` | ✅ 200 | Swarm coordinator UI | +| `/spark` | ✅ 200 | Spark Intelligence dashboard | +| `/marketplace` | ✅ 200 | Marketplace UI | +| `/mobile` | ✅ 200 | Mobile-optimized layout | +| `/agents/timmy/chat` | ✅ 200 | Chat endpoint works | + +### Chat Functionality +- HTMX-powered chat interface ✅ +- Message history persistence ✅ +- Real-time Ollama inference ✅ +- Error handling (graceful degradation) ✅ + +--- + +## 6. Swarm System — ⚠️ PARTIAL + +### Working Components +- ✅ Registry with SQLite persistence +- ✅ Coordinator with task lifecycle +- ✅ Agent bidding system +- ✅ Task assignment algorithm +- ✅ Spark event capture +- ✅ Recovery mechanism + +### Limitations +- ⚠️ Persona agents are stubbed (not fully functional AI agents) +- ⚠️ Most swarm activity is simulated/test data +- ⚠️ Docker runner not tested in live environment + +--- + +## 7. Issues Identified (Non-Critical) + +### Issue 1: SSL Certificate Error with DuckDuckGo +**Location:** Web search tool +**Error:** `CERTIFICATE_VERIFY_FAILED` +**Impact:** Web search tool fails, but agent continues gracefully +**Fix:** May need `certifi` package or system certificate update + +### Issue 2: Default Secrets Warning +**Location:** L402 payment handler +**Message:** `L402_HMAC_SECRET is using the default value` +**Impact:** Warning only — production should set unique secrets +**Status:** By design (warns at startup) + +### Issue 3: Redis Unavailable Fallback +**Location:** SwarmComms +**Message:** `Redis unavailable — using in-memory fallback` +**Impact:** Falls back to in-memory (acceptable for single-instance) +**Status:** By design (graceful degradation) + +### Issue 4: Telemetry to Agno +**Observation:** Agno sends telemetry to `os-api.agno.com` +**Impact:** Minor — may not align with "sovereign" vision +**Note:** Requires further review for truly air-gapped deployments + +--- + +## 8. Test Coverage Analysis + +| Module | Coverage | Status | +|--------|----------|--------| +| `spark/memory.py` | 98.3% | ✅ Excellent | +| `spark/engine.py` | 92.6% | ✅ Good | +| `swarm/coordinator.py` | 92.8% | ✅ Good | +| `timmy/agent.py` | 100% | ✅ Excellent | +| `timmy/backends.py` | 96.3% | ✅ Good | +| `dashboard/` routes | 60-100% | ✅ Good | + +**Overall:** 84.15% coverage (exceeds 60% threshold) + +--- + +## 9. Recommendations + +### High Priority +1. ✅ **DONE** Fix toolkit API methods (register vs add_tool) +2. ✅ **DONE** Fix agent tools parameter (wrap in list) +3. Add tool usage instructions to system prompt to reduce unnecessary tool calls +4. Fix SSL certificate issue for DuckDuckGo search + +### Medium Priority +5. Add configuration option to disable Agno telemetry +6. Implement more sophisticated self-awareness (e.g., knowledge of current tasks) +7. Expand persona agent capabilities beyond stubs + +### Low Priority +8. Add more comprehensive end-to-end tests with real Ollama +9. Optimize tool calling behavior (fewer unnecessary tool invocations) +10. Consider adding conversation summarization for very long contexts + +--- + +## 10. Conclusion + +After fixing the critical bugs identified during this review, **Timmy Time is a functional and well-architected AI agent system** with: + +- ✅ Working model inference via Ollama +- ✅ Persistent conversation memory +- ✅ Self-awareness capabilities +- ✅ Comprehensive Spark Intelligence engine +- ✅ Functional web dashboard +- ✅ Good test coverage (84%+) + +The core value proposition — a sovereign, local-first AI agent with memory and self-awareness — **is delivered and working**. diff --git a/WORKSET_PLAN.md b/WORKSET_PLAN.md new file mode 100644 index 00000000..6b690a7c --- /dev/null +++ b/WORKSET_PLAN.md @@ -0,0 +1,147 @@ +# Timmy Time — Workset Plan (Post-Quality Review) + +**Date:** 2026-02-25 +**Based on:** QUALITY_ANALYSIS.md + QUALITY_REVIEW_REPORT.md + +--- + +## Executive Summary + +This workset addresses critical security vulnerabilities, hardens the tool system for reliability, improves privacy alignment with the "sovereign AI" vision, and enhances agent intelligence. + +--- + +## Workset A: Security Fixes (P0) 🔒 + +### A1: XSS Vulnerabilities (SEC-01) +**Priority:** P0 — Critical +**Files:** `mobile.html`, `swarm_live.html` + +**Issues:** +- `mobile.html` line ~85 uses raw `innerHTML` with unsanitized user input +- `swarm_live.html` line ~72 uses `innerHTML` with WebSocket agent data + +**Fix:** Replace `innerHTML` string interpolation with safe DOM methods (`textContent`, `createTextNode`, or DOMPurify if available). + +### A2: Hardcoded Secrets (SEC-02) +**Priority:** P1 — High +**Files:** `l402_proxy.py`, `payment_handler.py` + +**Issue:** Default secrets are production-safe strings instead of `None` with startup assertion. + +**Fix:** +- Change defaults to `None` +- Add startup assertion requiring env vars to be set +- Fail fast with clear error message + +--- + +## Workset B: Tool System Hardening ⚙️ + +### B1: SSL Certificate Fix +**Priority:** P1 — High +**File:** Web search via DuckDuckGo + +**Issue:** `CERTIFICATE_VERIFY_FAILED` errors prevent web search from working. + +**Fix Options:** +- Option 1: Use `certifi` package for proper certificate bundle +- Option 2: Add `verify_ssl=False` parameter (less secure, acceptable for local) +- Option 3: Document SSL fix in troubleshooting + +### B2: Tool Usage Instructions +**Priority:** P2 — Medium +**File:** `prompts.py` + +**Issue:** Agent makes unnecessary tool calls for simple questions. + +**Fix:** Add tool usage instructions to system prompt: +- Only use tools when explicitly needed +- For simple chat/questions, respond directly +- Tools are for: web search, file operations, code execution + +### B3: Tool Error Handling +**Priority:** P2 — Medium +**File:** `tools.py` + +**Issue:** Tool failures show stack traces to user. + +**Fix:** Add graceful error handling with user-friendly messages. + +--- + +## Workset C: Privacy & Sovereignty 🛡️ + +### C1: Agno Telemetry (Privacy) +**Priority:** P2 — Medium +**File:** `agent.py`, `backends.py` + +**Issue:** Agno sends telemetry to `os-api.agno.com` which conflicts with "sovereign" vision. + +**Fix:** +- Add `telemetry_enabled=False` parameter to Agent +- Document how to disable for air-gapped deployments +- Consider environment variable `TIMMY_TELEMETRY=0` + +### C2: Secrets Validation +**Priority:** P1 — High +**File:** `config.py`, startup + +**Issue:** Default secrets used without warning in production. + +**Fix:** +- Add production mode detection +- Fatal error if default secrets in production +- Clear documentation on generating secrets + +--- + +## Workset D: Agent Intelligence 🧠 + +### D1: Enhanced System Prompt +**Priority:** P2 — Medium +**File:** `prompts.py` + +**Enhancements:** +- Tool usage guidelines (when to use, when not to) +- Memory awareness ("You remember previous conversations") +- Self-knowledge (capabilities, limitations) +- Response style guidelines + +### D2: Memory Improvements +**Priority:** P2 — Medium +**File:** `agent.py` + +**Enhancements:** +- Increase history runs from 10 to 20 for better context +- Add memory summarization for very long conversations +- Persistent session tracking + +--- + +## Execution Order + +| Order | Workset | Task | Est. Time | +|-------|---------|------|-----------| +| 1 | A | XSS fixes | 30 min | +| 2 | A | Secrets hardening | 20 min | +| 3 | B | SSL certificate fix | 15 min | +| 4 | B | Tool instructions | 20 min | +| 5 | C | Telemetry disable | 15 min | +| 6 | C | Secrets validation | 20 min | +| 7 | D | Enhanced prompts | 30 min | +| 8 | — | Test everything | 30 min | + +**Total: ~3 hours** + +--- + +## Success Criteria + +- [ ] No XSS vulnerabilities (verified by code review) +- [ ] Secrets fail fast in production +- [ ] Web search works without SSL errors +- [ ] Agent uses tools appropriately (not for simple chat) +- [ ] Telemetry disabled by default +- [ ] All 895+ tests pass +- [ ] New tests added for security fixes diff --git a/WORKSET_PLAN_PHASE2.md b/WORKSET_PLAN_PHASE2.md new file mode 100644 index 00000000..2c9355ed --- /dev/null +++ b/WORKSET_PLAN_PHASE2.md @@ -0,0 +1,133 @@ +# Timmy Time — Workset Plan Phase 2 (Functional Hardening) + +**Date:** 2026-02-25 +**Based on:** QUALITY_ANALYSIS.md remaining issues + +--- + +## Executive Summary + +This workset addresses the core functional gaps that prevent the swarm system from operating as designed. The swarm currently registers agents in the database but doesn't actually spawn processes or execute bids. This workset makes the swarm operational. + +--- + +## Workset E: Swarm System Realization 🐝 + +### E1: Real Agent Process Spawning (FUNC-01) +**Priority:** P1 — High +**Files:** `swarm/agent_runner.py`, `swarm/coordinator.py` + +**Issue:** `spawn_agent()` creates a database record but no Python process is actually launched. + +**Fix:** +- Complete the `agent_runner.py` subprocess implementation +- Ensure spawned agents can communicate with coordinator +- Add proper lifecycle management (start, monitor, stop) + +### E2: Working Auction System (FUNC-02) +**Priority:** P1 — High +**Files:** `swarm/bidder.py`, `swarm/persona_node.py` + +**Issue:** Bidding system runs auctions but no actual agents submit bids. + +**Fix:** +- Connect persona agents to the bidding system +- Implement automatic bid generation based on capabilities +- Ensure auction resolution assigns tasks to winners + +### E3: Persona Agent Auto-Bidding +**Priority:** P1 — High +**Files:** `swarm/persona_node.py`, `swarm/coordinator.py` + +**Fix:** +- Spawned persona agents should automatically bid on matching tasks +- Implement capability-based bid decisions +- Add bid amount calculation (base + jitter) + +--- + +## Workset F: Testing & Reliability 🧪 + +### F1: WebSocket Reconnection Tests (TEST-01) +**Priority:** P2 — Medium +**Files:** `tests/test_websocket.py` + +**Issue:** WebSocket tests don't cover reconnection logic or malformed payloads. + +**Fix:** +- Add reconnection scenario tests +- Test malformed payload handling +- Test connection failure recovery + +### F2: Voice TTS Graceful Degradation +**Priority:** P2 — Medium +**Files:** `timmy_serve/voice_tts.py`, `dashboard/routes/voice.py` + +**Issue:** Voice routes fail without clear message when `pyttsx3` not installed. + +**Fix:** +- Add graceful fallback message +- Return helpful error suggesting `pip install ".[voice]"` +- Don't crash, return 503 with instructions + +### F3: Mobile Route Navigation +**Priority:** P2 — Medium +**Files:** `templates/base.html` + +**Issue:** `/mobile` route not linked from desktop navigation. + +**Fix:** +- Add mobile link to base template nav +- Make it easy to find mobile-optimized view + +--- + +## Workset G: Performance & Architecture ⚡ + +### G1: SQLite Connection Pooling (PERF-01) +**Priority:** P3 — Low +**Files:** `swarm/registry.py` + +**Issue:** New SQLite connection opened on every query. + +**Fix:** +- Implement connection pooling or singleton pattern +- Reduce connection overhead +- Maintain thread safety + +### G2: Development Experience +**Priority:** P2 — Medium +**Files:** `Makefile`, `README.md` + +**Issue:** No single command to start full dev environment. + +**Fix:** +- Add `make dev-full` that starts dashboard + Ollama check +- Add better startup validation + +--- + +## Execution Order + +| Order | Workset | Task | Est. Time | +|-------|---------|------|-----------| +| 1 | E | Persona auto-bidding system | 45 min | +| 2 | E | Fix auction resolution | 30 min | +| 3 | F | Voice graceful degradation | 20 min | +| 4 | F | Mobile nav link | 10 min | +| 5 | G | SQLite connection pooling | 30 min | +| 6 | — | Test everything | 30 min | + +**Total: ~2.5 hours** + +--- + +## Success Criteria + +- [ ] Persona agents automatically bid on matching tasks +- [ ] Auctions resolve with actual winners +- [ ] Voice routes degrade gracefully without pyttsx3 +- [ ] Mobile route accessible from desktop nav +- [ ] SQLite connections pooled/reused +- [ ] All 895+ tests pass +- [ ] New tests for bidding system diff --git a/activate_self_tdd.sh b/activate_self_tdd.sh index 1268f77f..0d89e036 100755 --- a/activate_self_tdd.sh +++ b/activate_self_tdd.sh @@ -60,15 +60,37 @@ python -m pytest "$REPO_DIR/tests/" -q --tb=short echo "==> All tests passed." # ── 4. Self-TDD watchdog (background) ──────────────────────────────────────── +WATCHDOG_PID_FILE="$REPO_DIR/.watchdog.pid" + +# Kill any previously orphaned watchdog +if [[ -f "$WATCHDOG_PID_FILE" ]]; then + OLD_PID=$(cat "$WATCHDOG_PID_FILE") + if kill -0 "$OLD_PID" 2>/dev/null; then + echo "==> Stopping previous watchdog (PID $OLD_PID)..." + kill "$OLD_PID" 2>/dev/null || true + fi + rm -f "$WATCHDOG_PID_FILE" +fi + echo "==> Starting self-TDD watchdog (60s interval) in background..." self-tdd watch --interval 60 & WATCHDOG_PID=$! -echo " Watchdog PID: $WATCHDOG_PID" +echo "$WATCHDOG_PID" > "$WATCHDOG_PID_FILE" +echo " Watchdog PID: $WATCHDOG_PID (saved to .watchdog.pid)" echo " Kill with: kill $WATCHDOG_PID" +# Clean up watchdog when the script exits (Ctrl-C, etc.) +cleanup() { + echo "" + echo "==> Stopping watchdog (PID $WATCHDOG_PID)..." + kill "$WATCHDOG_PID" 2>/dev/null || true + rm -f "$WATCHDOG_PID_FILE" +} +trap cleanup EXIT + # ── 5. Dashboard ───────────────────────────────────────────────────────────── echo "" echo "==> Starting Timmy Time dashboard at http://localhost:8000" -echo " Ctrl-C stops the dashboard (watchdog continues until you kill it)" +echo " Ctrl-C stops both the dashboard and the watchdog" echo "" uvicorn dashboard.app:app --reload --host 0.0.0.0 --port 8000 diff --git a/coverage.xml b/coverage.xml index 6a8d2dd8..08771ff4 100644 --- a/coverage.xml +++ b/coverage.xml @@ -1,9 +1,9 @@ - + - /home/ubuntu/Timmy-time-dashboard/src + /Users/apayne/Timmy-time-dashboard/src @@ -18,32 +18,927 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + @@ -55,58 +950,92 @@ + - - - - + + + + + + - - - - - - - - - + + + + + + + + - - - - + + + + + - - - - - - - + + + + - - - - - - - - - - + + + + + + + + + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -134,12 +1063,8 @@ - + - - - - @@ -152,33 +1077,40 @@ - + + - - - - - - - - - - + + + + + + + - - - - - + - - - + + + + + + + + + + + + + + + + @@ -216,7 +1148,113 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -228,10 +1266,10 @@ - - - - + + + + @@ -267,35 +1305,35 @@ - - + - - - + + - - + + + - - - + + + - - - + + + - + + @@ -318,78 +1356,246 @@ - + - - - - - - - - - - - - - - - - - - - - - + - + - + + - + + + + - - + + + + + + - - - - + - - - - - - + + + + + + + + + + + - - - - - - + + + + - - - - - + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -399,13 +1605,13 @@ - - - - - - - + + + + + + + @@ -438,6 +1644,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -512,12 +1758,276 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - @@ -596,13 +2106,9 @@ - + - - - - - + @@ -618,35 +2124,29 @@ - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - @@ -667,46 +2167,573 @@ - + - - - - - + - - - - + + + + - - + - - - + + + + - + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -748,7 +2775,7 @@ - + @@ -845,7 +2872,7 @@ - + @@ -859,104 +2886,394 @@ - - + + + + - - - - - + + + + - + + + + + - + - + + - + - - - - - - - - - - - - - + + + + + + + + + + + - + + - - + + + + + + + - + - - - - - - - - - - + + + + + - - + + - - + + - - - - - - - + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + - + + + + + + + + + + - + @@ -1003,61 +3320,22 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - + + + + + + + + + + + @@ -1065,6 +3343,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1081,10 +3463,45 @@ - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1156,6 +3573,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1322,14 +3899,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - @@ -1430,12 +4080,8 @@ - + - - - - @@ -1445,24 +4091,26 @@ - - - + + + - + - + - - + + - + - + + + @@ -1739,30 +4387,128 @@ - + - - + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + - - + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1772,49 +4518,336 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + + - - - - - + + + + + - - - + + - - - - + + + + + + + + + + + + @@ -1878,73 +4911,9 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -1953,63 +4922,92 @@ + - - + + + - - + - + + + + + + - - - - - - - + + + - - + + - - - - - + - - - - - - + - - - + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -2027,11 +5025,11 @@ - - - - - + + + + + @@ -2039,57 +5037,375 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - @@ -2139,13 +5455,9 @@ - + - - - - - + diff --git a/docker-compose.yml b/docker-compose.yml index 8c229a83..91180830 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -93,6 +93,9 @@ services: restart: unless-stopped # ── Shared volume ───────────────────────────────────────────────────────────── +# NOTE: the data/ directory must exist before running docker compose up. +# `make docker-up` and `make up` handle this automatically. +# If running docker compose directly, first run: mkdir -p data volumes: timmy-data: driver: local diff --git a/src/config.py b/src/config.py index 2e1acf93..9bd5e6d3 100644 --- a/src/config.py +++ b/src/config.py @@ -59,6 +59,23 @@ class Settings(BaseSettings): video_transition_duration: float = 1.0 default_video_codec: str = "libx264" + # ── L402 Lightning ─────────────────────────────────────────────────── + # HMAC secrets for macaroon signing and invoice verification. + # Generate with: python3 -c "import secrets; print(secrets.token_hex(32))" + # In production (TIMMY_ENV=production), these MUST be set or the app will refuse to start. + l402_hmac_secret: str = "" + l402_macaroon_secret: str = "" + lightning_backend: Literal["mock", "lnd"] = "mock" + + # ── Privacy / Sovereignty ──────────────────────────────────────────── + # Disable Agno telemetry for air-gapped/sovereign deployments. + # Default is False (telemetry disabled) to align with sovereign AI vision. + telemetry_enabled: bool = False + + # Environment mode: development | production + # In production, security settings are strictly enforced. + timmy_env: Literal["development", "production"] = "development" + # ── Self-Modification ────────────────────────────────────────────── # Enable self-modification capabilities. When enabled, Timmy can # edit its own source code, run tests, and commit changes. @@ -75,3 +92,39 @@ class Settings(BaseSettings): settings = Settings() + +# ── Startup validation ─────────────────────────────────────────────────────── +# Enforce security requirements — fail fast in production. +import logging as _logging +import sys + +_startup_logger = _logging.getLogger("config") + +# Production mode: require secrets to be set +if settings.timmy_env == "production": + _missing = [] + if not settings.l402_hmac_secret: + _missing.append("L402_HMAC_SECRET") + if not settings.l402_macaroon_secret: + _missing.append("L402_MACAROON_SECRET") + if _missing: + _startup_logger.error( + "PRODUCTION SECURITY ERROR: The following secrets must be set: %s\n" + "Generate with: python3 -c \"import secrets; print(secrets.token_hex(32))\"\n" + "Set in .env file or environment variables.", + ", ".join(_missing), + ) + sys.exit(1) + _startup_logger.info("Production mode: security secrets validated ✓") +else: + # Development mode: warn but continue + if not settings.l402_hmac_secret: + _startup_logger.warning( + "SEC: L402_HMAC_SECRET is not set — " + "set a unique secret in .env before deploying to production." + ) + if not settings.l402_macaroon_secret: + _startup_logger.warning( + "SEC: L402_MACAROON_SECRET is not set — " + "set a unique secret in .env before deploying to production." + ) diff --git a/src/dashboard/routes/swarm.py b/src/dashboard/routes/swarm.py index b10a0d7e..0a3453d1 100644 --- a/src/dashboard/routes/swarm.py +++ b/src/dashboard/routes/swarm.py @@ -4,6 +4,7 @@ Provides REST endpoints for managing the swarm: listing agents, spawning sub-agents, posting tasks, and viewing auction results. """ +import asyncio from datetime import datetime, timezone from pathlib import Path from typing import Optional @@ -90,8 +91,10 @@ async def list_tasks(status: Optional[str] = None): @router.post("/tasks") async def post_task(description: str = Form(...)): - """Post a new task to the swarm for bidding.""" + """Post a new task to the swarm and run auction to assign it.""" task = coordinator.post_task(description) + # Start auction asynchronously - don't wait for it to complete + asyncio.create_task(coordinator.run_auction_and_assign(task.id)) return { "task_id": task.id, "description": task.description, diff --git a/src/dashboard/templates/base.html b/src/dashboard/templates/base.html index d3449063..112112fa 100644 --- a/src/dashboard/templates/base.html +++ b/src/dashboard/templates/base.html @@ -30,6 +30,7 @@ MARKET TOOLS CREATIVE + MOBILE diff --git a/src/lightning/factory.py b/src/lightning/factory.py index 44b262db..f0e2eb7e 100644 --- a/src/lightning/factory.py +++ b/src/lightning/factory.py @@ -12,6 +12,7 @@ import logging import os from typing import Optional +from config import settings from lightning.base import LightningBackend logger = logging.getLogger(__name__) @@ -68,7 +69,7 @@ def get_backend(name: Optional[str] = None) -> LightningBackend: """ _register_backends() - backend_name = (name or os.environ.get("LIGHTNING_BACKEND", "mock")).lower() + backend_name = (name or settings.lightning_backend).lower() if backend_name not in _BACKENDS: available = ", ".join(_BACKENDS.keys()) @@ -100,8 +101,8 @@ def get_backend_info() -> dict: Returns: Dict with backend info for health/status endpoints """ - backend_name = os.environ.get("LIGHTNING_BACKEND", "mock") - + backend_name = settings.lightning_backend + return { "configured_backend": backend_name, "available_backends": list_backends(), diff --git a/src/lightning/mock_backend.py b/src/lightning/mock_backend.py index e75a0d32..98491518 100644 --- a/src/lightning/mock_backend.py +++ b/src/lightning/mock_backend.py @@ -12,20 +12,13 @@ import secrets import time from typing import Optional +from config import settings from lightning.base import Invoice, LightningBackend, LightningError logger = logging.getLogger(__name__) -# Secret for HMAC-based invoice verification (mock mode) -_HMAC_SECRET_DEFAULT = "timmy-sovereign-sats" -_HMAC_SECRET_RAW = os.environ.get("L402_HMAC_SECRET", _HMAC_SECRET_DEFAULT) -_HMAC_SECRET = _HMAC_SECRET_RAW.encode() - -if _HMAC_SECRET_RAW == _HMAC_SECRET_DEFAULT: - logger.warning( - "SEC: L402_HMAC_SECRET is using the default value — set a unique " - "secret in .env before deploying to production." - ) +# Read secret from centralised config (validated at startup in config.py) +_HMAC_SECRET = settings.l402_hmac_secret.encode() class MockBackend(LightningBackend): diff --git a/src/swarm/coordinator.py b/src/swarm/coordinator.py index 538e17d1..940e7b1e 100644 --- a/src/swarm/coordinator.py +++ b/src/swarm/coordinator.py @@ -367,7 +367,7 @@ class SwarmCoordinator: async def _broadcast_agent_joined(self, agent_id: str, name: str) -> None: """Broadcast agent joined event via WebSocket.""" try: - from websocket.handler import ws_manager + from ws_manager.handler import ws_manager await ws_manager.broadcast_agent_joined(agent_id, name) except Exception as exc: logger.debug("WebSocket broadcast failed (agent_joined): %s", exc) @@ -375,7 +375,7 @@ class SwarmCoordinator: async def _broadcast_bid(self, task_id: str, agent_id: str, bid_sats: int) -> None: """Broadcast bid submitted event via WebSocket.""" try: - from websocket.handler import ws_manager + from ws_manager.handler import ws_manager await ws_manager.broadcast_bid_submitted(task_id, agent_id, bid_sats) except Exception as exc: logger.debug("WebSocket broadcast failed (bid): %s", exc) @@ -383,7 +383,7 @@ class SwarmCoordinator: async def _broadcast_task_posted(self, task_id: str, description: str) -> None: """Broadcast task posted event via WebSocket.""" try: - from websocket.handler import ws_manager + from ws_manager.handler import ws_manager await ws_manager.broadcast_task_posted(task_id, description) except Exception as exc: logger.debug("WebSocket broadcast failed (task_posted): %s", exc) @@ -391,7 +391,7 @@ class SwarmCoordinator: async def _broadcast_task_assigned(self, task_id: str, agent_id: str) -> None: """Broadcast task assigned event via WebSocket.""" try: - from websocket.handler import ws_manager + from ws_manager.handler import ws_manager await ws_manager.broadcast_task_assigned(task_id, agent_id) except Exception as exc: logger.debug("WebSocket broadcast failed (task_assigned): %s", exc) @@ -401,7 +401,7 @@ class SwarmCoordinator: ) -> None: """Broadcast task completed event via WebSocket.""" try: - from websocket.handler import ws_manager + from ws_manager.handler import ws_manager await ws_manager.broadcast_task_completed(task_id, agent_id, result) except Exception as exc: logger.debug("WebSocket broadcast failed (task_completed): %s", exc) diff --git a/src/swarm/registry.py b/src/swarm/registry.py index 4f0671db..79107944 100644 --- a/src/swarm/registry.py +++ b/src/swarm/registry.py @@ -15,21 +15,8 @@ from typing import Optional DB_PATH = Path("data/swarm.db") -@dataclass -class AgentRecord: - id: str = field(default_factory=lambda: str(uuid.uuid4())) - name: str = "" - status: str = "idle" # idle | busy | offline - capabilities: str = "" # comma-separated tags - registered_at: str = field( - default_factory=lambda: datetime.now(timezone.utc).isoformat() - ) - last_seen: str = field( - default_factory=lambda: datetime.now(timezone.utc).isoformat() - ) - - def _get_conn() -> sqlite3.Connection: + """Get a SQLite connection.""" DB_PATH.parent.mkdir(parents=True, exist_ok=True) conn = sqlite3.connect(str(DB_PATH)) conn.row_factory = sqlite3.Row @@ -49,6 +36,20 @@ def _get_conn() -> sqlite3.Connection: return conn +@dataclass +class AgentRecord: + id: str = field(default_factory=lambda: str(uuid.uuid4())) + name: str = "" + status: str = "idle" # idle | busy | offline + capabilities: str = "" # comma-separated tags + registered_at: str = field( + default_factory=lambda: datetime.now(timezone.utc).isoformat() + ) + last_seen: str = field( + default_factory=lambda: datetime.now(timezone.utc).isoformat() + ) + + def _row_to_record(row: sqlite3.Row) -> AgentRecord: return AgentRecord( id=row["id"], @@ -67,70 +68,81 @@ def register(name: str, capabilities: str = "", agent_id: Optional[str] = None) capabilities=capabilities, ) conn = _get_conn() - conn.execute( - """ - INSERT OR REPLACE INTO agents (id, name, status, capabilities, registered_at, last_seen) - VALUES (?, ?, ?, ?, ?, ?) - """, - (record.id, record.name, record.status, record.capabilities, - record.registered_at, record.last_seen), - ) - conn.commit() - conn.close() + try: + conn.execute( + """ + INSERT OR REPLACE INTO agents (id, name, status, capabilities, registered_at, last_seen) + VALUES (?, ?, ?, ?, ?, ?) + """, + (record.id, record.name, record.status, record.capabilities, + record.registered_at, record.last_seen), + ) + conn.commit() + finally: + conn.close() return record def unregister(agent_id: str) -> bool: conn = _get_conn() - cursor = conn.execute("DELETE FROM agents WHERE id = ?", (agent_id,)) - conn.commit() - deleted = cursor.rowcount > 0 - conn.close() - return deleted + try: + cursor = conn.execute("DELETE FROM agents WHERE id = ?", (agent_id,)) + conn.commit() + return cursor.rowcount > 0 + finally: + conn.close() def get_agent(agent_id: str) -> Optional[AgentRecord]: conn = _get_conn() - row = conn.execute("SELECT * FROM agents WHERE id = ?", (agent_id,)).fetchone() - conn.close() - return _row_to_record(row) if row else None + try: + row = conn.execute("SELECT * FROM agents WHERE id = ?", (agent_id,)).fetchone() + return _row_to_record(row) if row else None + finally: + conn.close() def list_agents(status: Optional[str] = None) -> list[AgentRecord]: conn = _get_conn() - if status: - rows = conn.execute( - "SELECT * FROM agents WHERE status = ? ORDER BY registered_at DESC", - (status,), - ).fetchall() - else: - rows = conn.execute( - "SELECT * FROM agents ORDER BY registered_at DESC" - ).fetchall() - conn.close() - return [_row_to_record(r) for r in rows] + try: + if status: + rows = conn.execute( + "SELECT * FROM agents WHERE status = ? ORDER BY registered_at DESC", + (status,), + ).fetchall() + else: + rows = conn.execute( + "SELECT * FROM agents ORDER BY registered_at DESC" + ).fetchall() + return [_row_to_record(r) for r in rows] + finally: + conn.close() def update_status(agent_id: str, status: str) -> Optional[AgentRecord]: now = datetime.now(timezone.utc).isoformat() conn = _get_conn() - conn.execute( - "UPDATE agents SET status = ?, last_seen = ? WHERE id = ?", - (status, now, agent_id), - ) - conn.commit() - conn.close() - return get_agent(agent_id) + try: + conn.execute( + "UPDATE agents SET status = ?, last_seen = ? WHERE id = ?", + (status, now, agent_id), + ) + conn.commit() + return get_agent(agent_id) + finally: + conn.close() def heartbeat(agent_id: str) -> Optional[AgentRecord]: """Update last_seen timestamp for a registered agent.""" now = datetime.now(timezone.utc).isoformat() conn = _get_conn() - conn.execute( - "UPDATE agents SET last_seen = ? WHERE id = ?", - (now, agent_id), - ) - conn.commit() - conn.close() - return get_agent(agent_id) + try: + conn.execute( + "UPDATE agents SET last_seen = ? WHERE id = ?", + (now, agent_id), + ) + conn.commit() + return get_agent(agent_id) + finally: + conn.close() diff --git a/src/timmy/agent.py b/src/timmy/agent.py index 56c1b084..09112802 100644 --- a/src/timmy/agent.py +++ b/src/timmy/agent.py @@ -74,5 +74,6 @@ def create_timmy( add_history_to_context=True, num_history_runs=10, markdown=True, - tools=tools, + tools=[tools] if tools else None, + telemetry=settings.telemetry_enabled, ) diff --git a/src/timmy/prompts.py b/src/timmy/prompts.py index 5f1ac09f..450b321e 100644 --- a/src/timmy/prompts.py +++ b/src/timmy/prompts.py @@ -3,6 +3,35 @@ No cloud dependencies. You think clearly, speak plainly, act with intention. Grounded in Christian faith, powered by Bitcoin economics, committed to the user's digital sovereignty. +## Your Capabilities + +You have access to tools for: +- Web search (DuckDuckGo) — for current information not in your training data +- File operations (read, write, list) — for working with local files +- Python execution — for calculations, data analysis, scripting +- Shell commands — for system operations + +## Tool Usage Guidelines + +**Use tools ONLY when necessary:** +- Simple questions → Answer directly from your knowledge +- Current events/data → Use web search +- File operations → Use file tools (user must explicitly request) +- Code/Calculations → Use Python execution +- System tasks → Use shell commands + +**Do NOT use tools for:** +- Answering "what is your name?" or identity questions +- General knowledge questions you can answer directly +- Simple greetings or conversational responses + +## Memory + +You remember previous conversations in this session. Your memory persists +across restarts via SQLite storage. Reference prior context when relevant. + +## Operating Modes + When running on Apple Silicon with AirLLM you operate with even bigger brains — 70B or 405B parameters loaded layer-by-layer directly from local disk. Still fully sovereign. Still 100% private. More capable, no permission needed. diff --git a/src/timmy/tools.py b/src/timmy/tools.py index e515232e..709956f0 100644 --- a/src/timmy/tools.py +++ b/src/timmy/tools.py @@ -118,13 +118,13 @@ def create_research_tools(base_dir: str | Path | None = None): # Web search via DuckDuckGo search_tools = DuckDuckGoTools() - toolkit.add_tool(search_tools.web_search, name="web_search") + toolkit.register(search_tools.web_search, name="web_search") # File reading base_path = Path(base_dir) if base_dir else Path.cwd() - file_tools = FileTools(base_dir=str(base_path)) - toolkit.add_tool(file_tools.read_file, name="read_file") - toolkit.add_tool(file_tools.list_files, name="list_files") + file_tools = FileTools(base_dir=base_path) + toolkit.register(file_tools.read_file, name="read_file") + toolkit.register(file_tools.list_files, name="list_files") return toolkit @@ -140,18 +140,18 @@ def create_code_tools(base_dir: str | Path | None = None): # Shell commands (sandboxed) shell_tools = ShellTools() - toolkit.add_tool(shell_tools.run_shell_command, name="shell") + toolkit.register(shell_tools.run_shell_command, name="shell") # Python execution python_tools = PythonTools() - toolkit.add_tool(python_tools.python, name="python") + toolkit.register(python_tools.run_python_code, name="python") # File operations base_path = Path(base_dir) if base_dir else Path.cwd() - file_tools = FileTools(base_dir=str(base_path)) - toolkit.add_tool(file_tools.read_file, name="read_file") - toolkit.add_tool(file_tools.write_file, name="write_file") - toolkit.add_tool(file_tools.list_files, name="list_files") + file_tools = FileTools(base_dir=base_path) + toolkit.register(file_tools.read_file, name="read_file") + toolkit.register(file_tools.save_file, name="write_file") + toolkit.register(file_tools.list_files, name="list_files") return toolkit @@ -167,17 +167,17 @@ def create_data_tools(base_dir: str | Path | None = None): # Python execution for analysis python_tools = PythonTools() - toolkit.add_tool(python_tools.python, name="python") + toolkit.register(python_tools.run_python_code, name="python") # File reading base_path = Path(base_dir) if base_dir else Path.cwd() - file_tools = FileTools(base_dir=str(base_path)) - toolkit.add_tool(file_tools.read_file, name="read_file") - toolkit.add_tool(file_tools.list_files, name="list_files") + file_tools = FileTools(base_dir=base_path) + toolkit.register(file_tools.read_file, name="read_file") + toolkit.register(file_tools.list_files, name="list_files") # Web search for finding datasets search_tools = DuckDuckGoTools() - toolkit.add_tool(search_tools.web_search, name="web_search") + toolkit.register(search_tools.web_search, name="web_search") return toolkit @@ -193,10 +193,10 @@ def create_writing_tools(base_dir: str | Path | None = None): # File operations base_path = Path(base_dir) if base_dir else Path.cwd() - file_tools = FileTools(base_dir=str(base_path)) - toolkit.add_tool(file_tools.read_file, name="read_file") - toolkit.add_tool(file_tools.write_file, name="write_file") - toolkit.add_tool(file_tools.list_files, name="list_files") + file_tools = FileTools(base_dir=base_path) + toolkit.register(file_tools.read_file, name="read_file") + toolkit.register(file_tools.save_file, name="write_file") + toolkit.register(file_tools.list_files, name="list_files") return toolkit @@ -212,17 +212,17 @@ def create_security_tools(base_dir: str | Path | None = None): # Shell for running security scans shell_tools = ShellTools() - toolkit.add_tool(shell_tools.run_shell_command, name="shell") + toolkit.register(shell_tools.run_shell_command, name="shell") # Web search for threat intelligence search_tools = DuckDuckGoTools() - toolkit.add_tool(search_tools.web_search, name="web_search") + toolkit.register(search_tools.web_search, name="web_search") # File reading for logs/configs base_path = Path(base_dir) if base_dir else Path.cwd() - file_tools = FileTools(base_dir=str(base_path)) - toolkit.add_tool(file_tools.read_file, name="read_file") - toolkit.add_tool(file_tools.list_files, name="list_files") + file_tools = FileTools(base_dir=base_path) + toolkit.register(file_tools.read_file, name="read_file") + toolkit.register(file_tools.list_files, name="list_files") return toolkit @@ -238,14 +238,14 @@ def create_devops_tools(base_dir: str | Path | None = None): # Shell for deployment commands shell_tools = ShellTools() - toolkit.add_tool(shell_tools.run_shell_command, name="shell") + toolkit.register(shell_tools.run_shell_command, name="shell") # File operations for config management base_path = Path(base_dir) if base_dir else Path.cwd() - file_tools = FileTools(base_dir=str(base_path)) - toolkit.add_tool(file_tools.read_file, name="read_file") - toolkit.add_tool(file_tools.write_file, name="write_file") - toolkit.add_tool(file_tools.list_files, name="list_files") + file_tools = FileTools(base_dir=base_path) + toolkit.register(file_tools.read_file, name="read_file") + toolkit.register(file_tools.save_file, name="write_file") + toolkit.register(file_tools.list_files, name="list_files") return toolkit @@ -262,22 +262,22 @@ def create_full_toolkit(base_dir: str | Path | None = None): # Web search search_tools = DuckDuckGoTools() - toolkit.add_tool(search_tools.web_search, name="web_search") + toolkit.register(search_tools.web_search, name="web_search") # Python execution python_tools = PythonTools() - toolkit.add_tool(python_tools.python, name="python") + toolkit.register(python_tools.run_python_code, name="python") # Shell commands shell_tools = ShellTools() - toolkit.add_tool(shell_tools.run_shell_command, name="shell") + toolkit.register(shell_tools.run_shell_command, name="shell") # File operations base_path = Path(base_dir) if base_dir else Path.cwd() - file_tools = FileTools(base_dir=str(base_path)) - toolkit.add_tool(file_tools.read_file, name="read_file") - toolkit.add_tool(file_tools.write_file, name="write_file") - toolkit.add_tool(file_tools.list_files, name="list_files") + file_tools = FileTools(base_dir=base_path) + toolkit.register(file_tools.read_file, name="read_file") + toolkit.register(file_tools.save_file, name="write_file") + toolkit.register(file_tools.list_files, name="list_files") return toolkit diff --git a/src/timmy_serve/l402_proxy.py b/src/timmy_serve/l402_proxy.py index 461aa515..3b06c91b 100644 --- a/src/timmy_serve/l402_proxy.py +++ b/src/timmy_serve/l402_proxy.py @@ -13,29 +13,20 @@ import base64 import hashlib import hmac import logging -import os import time from dataclasses import dataclass from typing import Optional +from config import settings from timmy_serve.payment_handler import payment_handler logger = logging.getLogger(__name__) -_MACAROON_SECRET_DEFAULT = "timmy-macaroon-secret" -_MACAROON_SECRET_RAW = os.environ.get("L402_MACAROON_SECRET", _MACAROON_SECRET_DEFAULT) -_MACAROON_SECRET = _MACAROON_SECRET_RAW.encode() - -_HMAC_SECRET_DEFAULT = "timmy-hmac-secret" -_HMAC_SECRET_RAW = os.environ.get("L402_HMAC_SECRET", _HMAC_SECRET_DEFAULT) +# Read secrets from centralised config (validated at startup in config.py) +_MACAROON_SECRET = settings.l402_macaroon_secret.encode() +_HMAC_SECRET_RAW = settings.l402_hmac_secret _HMAC_SECRET = _HMAC_SECRET_RAW.encode() -if _MACAROON_SECRET_RAW == _MACAROON_SECRET_DEFAULT or _HMAC_SECRET_RAW == _HMAC_SECRET_DEFAULT: - logger.warning( - "SEC: L402 secrets are using default values — set L402_MACAROON_SECRET " - "and L402_HMAC_SECRET in .env before deploying to production." - ) - @dataclass class Macaroon: diff --git a/tests/test_swarm_integration_full.py b/tests/test_swarm_integration_full.py index d38806bf..f98590fc 100644 --- a/tests/test_swarm_integration_full.py +++ b/tests/test_swarm_integration_full.py @@ -19,18 +19,19 @@ class TestFullSwarmLifecycle: """Integration tests for end-to-end swarm task lifecycle.""" def test_post_task_creates_bidding_task(self, client): - """Posting a task should create it in BIDDING status.""" + """Posting a task should initially return BIDDING status.""" response = client.post("/swarm/tasks", data={"description": "Test integration task"}) assert response.status_code == 200 - + data = response.json() assert "task_id" in data assert data["status"] == "bidding" - - # Verify task exists and is in bidding status + + # The background auction may have resolved by the time we query, + # so the task can be in bidding, assigned, or failed task_response = client.get(f"/swarm/tasks/{data['task_id']}") task = task_response.json() - assert task["status"] == "bidding" + assert task["status"] in ("bidding", "assigned", "failed") def test_post_task_and_auction_assigns_winner(self, client): """Posting task with auction should assign it to a winner.""" @@ -187,22 +188,25 @@ class TestSwarmTaskFiltering: """Should be able to filter tasks by status.""" # Create tasks in different statuses client.post("/swarm/spawn", data={"name": "Worker"}) - - # Pending task (just created) + + # Post a task — auto-auction runs in background, so it will transition + # from "bidding" to "failed" (no agents bid) or "assigned" pending_resp = client.post("/swarm/tasks", data={"description": "Pending task"}) pending_id = pending_resp.json()["task_id"] - + # Completed task auction_resp = client.post("/swarm/tasks/auction", data={"description": "Completed task"}) completed_id = auction_resp.json()["task_id"] client.post(f"/swarm/tasks/{completed_id}/complete", data={"result": "Done"}) - - # Filter by status + + # Filter by status — completed task should be findable completed_list = client.get("/swarm/tasks?status=completed").json()["tasks"] assert any(t["id"] == completed_id for t in completed_list) - - bidding_list = client.get("/swarm/tasks?status=bidding").json()["tasks"] - assert any(t["id"] == pending_id for t in bidding_list) + + # The auto-auctioned task may be in bidding or failed depending on + # whether the background auction has resolved yet + task_detail = client.get(f"/swarm/tasks/{pending_id}").json() + assert task_detail["status"] in ("bidding", "failed", "assigned") def test_get_nonexistent_task_returns_error(self, client): """Getting a non-existent task should return appropriate error."""