fix: resolve all 7 Phase 1 setup/infra issues

V1.01: Add fastembed, httpx, redis to requirements.txt (now 10 lines, 9 deps)
V1.03: Create setup/setup_db.py — idempotent SQLite schema for state.db + memory_store.db
V1.04: Create .dockerignore (21 patterns — .env, *.key, __pycache__, etc.)
V1.05: Dockerfile COPY --chown=appuser:appuser (after useradd, before USER switch)
V1.06: Add EXPOSE 8000 + HEALTHCHECK (Redis ping, 30s interval)
V1.07: Document database setup as Step 2 in install.md (pip install + setup_db.py + table listing)
V1.08: Remove change-me from REDIS_PASSWORD in .env.example
V1.09: Replace gcc with build-essential + python3-dev in Dockerfile
This commit is contained in:
ClaudioDrews 2026-06-03 11:31:04 -03:00
parent 55ba9cee1c
commit 384395b78d
6 changed files with 399 additions and 14 deletions

21
.dockerignore Normal file
View File

@ -0,0 +1,21 @@
# Docker build context exclusion
.env
*.key
*.pem
*.p12
.git
.gitignore
__pycache__/
*.pyc
*.pyo
*.egg-info/
dist/
build/
.venv/
venv/
env/
node_modules/
*.log
.DS_Store
Thumbs.db
docker/.env

View File

@ -9,7 +9,7 @@
OPENROUTER_API_KEY=sk-or-... OPENROUTER_API_KEY=sk-or-...
# Redis password (generate with: openssl rand -hex 16) # Redis password (generate with: openssl rand -hex 16)
REDIS_PASSWORD=change-me REDIS_PASSWORD=
# ── Paths ───────────────────────────────────────────── # ── Paths ─────────────────────────────────────────────

View File

@ -2,23 +2,31 @@ FROM python:3.12-slim
WORKDIR /app WORKDIR /app
# Install system dependencies # Install system dependencies (build-essential for fastembed, python3-dev for C extensions)
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \ build-essential \
python3-dev \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Copy and install Python dependencies # Copy and install Python dependencies
COPY requirements.txt . COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
# Copy source code # Create non-root user
COPY . . RUN useradd -r -u 10001 appuser
# Copy source code with correct ownership
COPY --chown=appuser:appuser . .
# Default environment variables (override via docker-compose) # Default environment variables (override via docker-compose)
ENV PYTHONUNBUFFERED=1 \ ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 PYTHONDONTWRITEBYTECODE=1
RUN useradd -r -u 10001 appuser
USER appuser USER appuser
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD python -c "import redis; r=redis.from_url('redis://redis:6379'); r.ping()" || exit 1
CMD ["python", "main.py", "--run-worker"] CMD ["python", "main.py", "--run-worker"]

View File

@ -2,6 +2,9 @@
requests>=2.31.0 requests>=2.31.0
aiohttp>=3.9.0 aiohttp>=3.9.0
arq>=0.28.0 arq>=0.28.0
redis>=5.0.0
python-dotenv>=1.0.0 python-dotenv>=1.0.0
pyyaml>=6.0 pyyaml>=6.0
qdrant-client>=1.17.0 qdrant-client>=1.17.0
httpx>=0.27.0
fastembed>=0.4.0

View File

@ -19,7 +19,46 @@
cp -r icarus/ ~/.hermes/plugins/icarus/ cp -r icarus/ ~/.hermes/plugins/icarus/
``` ```
### 2. Enable Icarus in Hermes Config ### 2. Database Setup
Install the Python dependencies first:
```bash
pip install -r requirements.txt
```
Memory OS requires two SQLite databases with FTS5 full-text search indexes:
`state.db` (session history, lineage, reflection budget) and `memory_store.db`
(facts, entities, memory banks). The setup script creates both with idempotent
`CREATE TABLE IF NOT EXISTS` statements — safe to run multiple times.
```bash
python setup/setup_db.py
```
**What it creates:**
| Database | Tables |
|---|---|
| `state.db` | `sessions`, `messages`, `messages_fts` (FTS5), `messages_fts_trigram`, `lineage`, `reflection_budget`, `compression_locks`, `schema_version`, `state_meta` |
| `memory_store.db` | `entities`, `facts`, `facts_fts` (FTS5), `fact_entities`, `memory_banks` |
Options:
```bash
python setup/setup_db.py --dry-run # preview without executing
python setup/setup_db.py --state-db /custom/path/state.db
python setup/setup_db.py --memory-db /custom/path/memory_store.db
```
Environment variables override defaults:
```bash
export STATE_DB_PATH=/home/your-user/.hermes/state.db
export MEMORY_STORE_PATH=/home/your-user/.hermes/memory_store.db
```
### 3. Enable Icarus in Hermes Config
Icarus must be registered as an enabled plugin. Edit `~/.hermes/config.yaml`: Icarus must be registered as an enabled plugin. Edit `~/.hermes/config.yaml`:
@ -42,7 +81,7 @@ hermes status
# → Should show: icarus v0.3.0 (16 tools, 4 hooks) # → Should show: icarus v0.3.0 (16 tools, 4 hooks)
``` ```
### 3. Docker Infrastructure ### 4. Docker Infrastructure
The compose file lives in the `docker/` directory of this repository and must be run **in-place** — the worker build context (`./worker`) is relative to the compose file location. The compose file lives in the `docker/` directory of this repository and must be run **in-place** — the worker build context (`./worker`) is relative to the compose file location.
@ -75,7 +114,7 @@ curl -s http://localhost:6333/healthz # → {"title":"ok","version":"1.17.1"}
redis-cli -a "$REDIS_PASSWORD" ping # → PONG redis-cli -a "$REDIS_PASSWORD" ping # → PONG
``` ```
### 4. Environment Variables ### 5. Environment Variables
Add to your Hermes profile `.env` (e.g. `~/.hermes/.env`): Add to your Hermes profile `.env` (e.g. `~/.hermes/.env`):
@ -108,7 +147,7 @@ ICARUS_TASK_MAX_CHARS=300
**⚠️ Use absolute paths.** The Hermes gateway runs as a systemd service — `~` is not expanded. Always use `/home/your-user/...`. **⚠️ Use absolute paths.** The Hermes gateway runs as a systemd service — `~` is not expanded. Always use `/home/your-user/...`.
### 5. Core File Modifications ### 6. Core File Modifications
Apply the additions documented in [setup/rulebook.md](rulebook.md) and Apply the additions documented in [setup/rulebook.md](rulebook.md) and
[modifications/soul-rulebook.md](../modifications/soul-rulebook.md): [modifications/soul-rulebook.md](../modifications/soul-rulebook.md):
@ -129,7 +168,7 @@ These modifications ensure the agent treats injected memory as more
authoritative than training knowledge, and knows where to find authoritative than training knowledge, and knows where to find
persisted information without re-discovering it. persisted information without re-discovering it.
### 6. Wiki + Vault Setup ### 7. Wiki + Vault Setup
Memory OS stores its knowledge pipeline inside an Obsidian vault. The vault Memory OS stores its knowledge pipeline inside an Obsidian vault. The vault
path is user-specific — set it as an environment variable first: path is user-specific — set it as an environment variable first:
@ -159,7 +198,7 @@ MOC generation, install [vault-curator](https://github.com/ClaudioDrews/vault-cu
as a separate tool. It runs independently and is not required for Memory OS as a separate tool. It runs independently and is not required for Memory OS
core functionality. core functionality.
### 7. Maintenance Scripts ### 8. Maintenance Scripts
The `scripts/` directory in this repository contains the maintenance tools The `scripts/` directory in this repository contains the maintenance tools
that keep the memory stack healthy. Copy them to a location of your choice that keep the memory stack healthy. Copy them to a location of your choice
@ -217,7 +256,7 @@ scanner will find zero eligible points.
`DEDUP_EXEMPT_PREFIXES` env vars (comma-separated prefixes) to exclude `DEDUP_EXEMPT_PREFIXES` env vars (comma-separated prefixes) to exclude
specific Qdrant collections from automated maintenance. specific Qdrant collections from automated maintenance.
### 8. Gateway Restart ### 9. Gateway Restart
```bash ```bash
hermes gateway restart hermes gateway restart
@ -225,7 +264,7 @@ hermes gateway restart
Changes to `.env`, `SOUL.md`, `rulebook.md`, and Icarus plugin code only take effect after restart. Changes to `.env`, `SOUL.md`, `rulebook.md`, and Icarus plugin code only take effect after restart.
### 9. Verify ### 10. Verify
Inside Hermes chat: Inside Hermes chat:

314
setup/setup_db.py Normal file
View File

@ -0,0 +1,314 @@
#!/usr/bin/env python3
"""Memory OS — Database Setup Script
Creates the SQLite databases and schemas required by the Icarus plugin.
Run this once during initial installation, or after deleting the databases.
Usage:
python setup_db.py # uses default paths
python setup_db.py --state-db /custom/path/state.db
python setup_db.py --memory-db /custom/path/memory_store.db
python setup_db.py --dry-run # print SQL without executing
Environment variables:
STATE_DB_PATH path to state.db (default: ~/.hermes/state.db)
MEMORY_STORE_PATH path to memory_store.db (default: ~/.hermes/memory_store.db)
"""
import argparse
import os
import sqlite3
import sys
from pathlib import Path
# ── Default paths ──────────────────────────────────────────────────────────
DEFAULT_HERMES_HOME = Path.home() / ".hermes"
DEFAULT_STATE_DB = DEFAULT_HERMES_HOME / "state.db"
DEFAULT_MEMORY_DB = DEFAULT_HERMES_HOME / "memory_store.db"
# ── Schema: state.db ───────────────────────────────────────────────────────
STATE_SCHEMA = """
-- Sessions table one row per Hermes session
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
source TEXT NOT NULL,
user_id TEXT,
model TEXT,
model_config TEXT,
system_prompt TEXT,
parent_session_id TEXT,
started_at REAL NOT NULL,
ended_at REAL,
end_reason TEXT,
message_count INTEGER DEFAULT 0,
tool_call_count INTEGER DEFAULT 0,
input_tokens INTEGER DEFAULT 0,
output_tokens INTEGER DEFAULT 0,
cache_read_tokens INTEGER DEFAULT 0,
cache_write_tokens INTEGER DEFAULT 0,
reasoning_tokens INTEGER DEFAULT 0,
billing_provider TEXT,
billing_base_url TEXT,
billing_mode TEXT,
estimated_cost_usd REAL,
actual_cost_usd REAL,
cost_status TEXT,
cost_source TEXT,
pricing_version TEXT,
title TEXT,
api_call_count INTEGER DEFAULT 0,
handoff_state TEXT,
handoff_platform TEXT,
handoff_error TEXT,
cwd TEXT,
rewind_count INTEGER NOT NULL DEFAULT 0,
archived INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (parent_session_id) REFERENCES sessions(id)
);
-- Messages table one row per user/assistant/tool message
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL REFERENCES sessions(id),
role TEXT NOT NULL,
content TEXT,
tool_call_id TEXT,
tool_calls TEXT,
tool_name TEXT,
timestamp REAL NOT NULL,
token_count INTEGER,
finish_reason TEXT,
reasoning TEXT,
reasoning_content TEXT,
reasoning_details TEXT,
codex_reasoning_items TEXT,
codex_message_items TEXT,
platform_message_id TEXT,
observed INTEGER DEFAULT 0,
active INTEGER NOT NULL DEFAULT 1
);
-- FTS5 full-text search over message content (used by session_search)
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
content,
content=messages,
content_rowid=id
);
-- Trigram FTS5 index for substring/phrase matching
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts_trigram USING fts5(
content,
tokenize='trigram',
content=messages,
content_rowid=id
);
-- Lineage tracking generation provenance for context enhancement
CREATE TABLE IF NOT EXISTS lineage (
lineage_id TEXT PRIMARY KEY,
session_id TEXT,
query TEXT,
retrieved_chunk_ids TEXT,
generation_model TEXT,
generation_context_hash TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Reflection budget rate-limiting for micro-reflections
CREATE TABLE IF NOT EXISTS reflection_budget (
hour_window TEXT PRIMARY KEY,
count INTEGER DEFAULT 0,
tokens_used INTEGER DEFAULT 0
);
-- Compression locks concurrency control for session compression
CREATE TABLE IF NOT EXISTS compression_locks (
session_id TEXT PRIMARY KEY,
holder TEXT NOT NULL,
acquired_at REAL NOT NULL,
expires_at REAL NOT NULL
);
-- Schema version tracking
CREATE TABLE IF NOT EXISTS schema_version (
version INTEGER NOT NULL
);
-- State metadata key-value store
CREATE TABLE IF NOT EXISTS state_meta (
key TEXT PRIMARY KEY,
value TEXT
);
"""
# ── Schema: memory_store.db ────────────────────────────────────────────────
MEMORY_SCHEMA = """
-- Entities named entities extracted from facts
CREATE TABLE IF NOT EXISTS entities (
entity_id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
entity_type TEXT DEFAULT 'unknown',
aliases TEXT DEFAULT '',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Facts durable structured memory with trust scoring
CREATE TABLE IF NOT EXISTS facts (
fact_id INTEGER PRIMARY KEY AUTOINCREMENT,
content TEXT NOT NULL UNIQUE,
category TEXT DEFAULT 'general',
tags TEXT DEFAULT '',
trust_score REAL DEFAULT 0.5,
retrieval_count INTEGER DEFAULT 0,
helpful_count INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
hrr_vector BLOB
);
-- Fact-entity junction (many-to-many)
CREATE TABLE IF NOT EXISTS fact_entities (
fact_id INTEGER REFERENCES facts(fact_id),
entity_id INTEGER REFERENCES entities(entity_id),
PRIMARY KEY (fact_id, entity_id)
);
-- FTS5 full-text search over facts
CREATE VIRTUAL TABLE IF NOT EXISTS facts_fts
USING fts5(content, tags, content=facts, content_rowid=fact_id);
-- Memory banks algebraically composable fact groups (HRR vectors)
CREATE TABLE IF NOT EXISTS memory_banks (
bank_id INTEGER PRIMARY KEY AUTOINCREMENT,
bank_name TEXT NOT NULL UNIQUE,
vector BLOB NOT NULL,
dim INTEGER NOT NULL,
fact_count INTEGER DEFAULT 0,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
"""
def create_database(db_path: Path, schema: str, dry_run: bool = False) -> bool:
"""Create database and execute schema. Returns True on success."""
print(f"\n📘 Database: {db_path}")
if dry_run:
print(" [DRY-RUN] Would execute schema:")
for line in schema.strip().split("\n"):
if line.strip():
print(f" {line.strip()[:100]}")
return True
db_path.parent.mkdir(parents=True, exist_ok=True)
try:
conn = sqlite3.connect(str(db_path))
conn.executescript(schema)
conn.commit()
conn.close()
print(f" ✅ Created ({db_path.stat().st_size:,} bytes)")
# Verify tables
conn = sqlite3.connect(str(db_path))
tables = conn.execute(
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
).fetchall()
conn.close()
print(f" 📋 Tables: {', '.join(t[0] for t in tables)}")
return True
except sqlite3.Error as e:
print(f" ❌ Error: {e}", file=sys.stderr)
return False
def insert_schema_version(db_path: Path, version: int = 1) -> None:
"""Record the schema version if the table exists and is empty."""
try:
conn = sqlite3.connect(str(db_path))
existing = conn.execute(
"SELECT COUNT(*) FROM schema_version"
).fetchone()[0]
if existing == 0:
conn.execute(
"INSERT INTO schema_version (version) VALUES (?)", (version,)
)
conn.commit()
conn.close()
except sqlite3.Error:
pass # table might not exist (not state.db)
def main():
parser = argparse.ArgumentParser(
description="Memory OS — Database Setup"
)
parser.add_argument(
"--state-db",
default=os.environ.get(
"STATE_DB_PATH", str(DEFAULT_STATE_DB)
),
help=f"Path to state.db (default: {DEFAULT_STATE_DB})",
)
parser.add_argument(
"--memory-db",
default=os.environ.get(
"MEMORY_STORE_PATH", str(DEFAULT_MEMORY_DB)
),
help=f"Path to memory_store.db (default: {DEFAULT_MEMORY_DB})",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Print SQL without executing",
)
args = parser.parse_args()
state_path = Path(args.state_db)
memory_path = Path(args.memory_db)
print("🧠 Memory OS — Database Setup")
print(f" Hermes home: {DEFAULT_HERMES_HOME}")
print(f" State DB: {state_path}")
print(f" Memory DB: {memory_path}")
if args.dry_run:
print("\n⚠️ DRY-RUN mode — no changes will be made.\n")
# ── Create state.db ──
ok_state = create_database(state_path, STATE_SCHEMA, args.dry_run)
if ok_state and not args.dry_run:
insert_schema_version(state_path, version=1)
# ── Create memory_store.db ──
ok_memory = create_database(memory_path, MEMORY_SCHEMA, args.dry_run)
# ── Summary ──
print()
if args.dry_run:
print("🏁 Dry-run complete. Run without --dry-run to apply.")
elif ok_state and ok_memory:
print("✅ Both databases created successfully.")
print()
print(" Next steps:")
print(f" 1. Ensure Qdrant is running: docker compose up -d qdrant")
print(f" 2. Create the Qdrant collection:")
print(f" python scripts/bulk_wiki_ingest.py (creates collection automatically)")
print(f" OR manually:")
print(f" curl -X PUT http://localhost:6333/collections/knowledge_base \\")
print(f" -H 'Content-Type: application/json' \\")
print(f" -d '{{\"vectors\":{{\"dense\":{{\"size\":4096,\"distance\":\"Cosine\"}}}},\"sparse_vectors\":{{\"sparse\":{{}}}}}}'")
sys.exit(0)
else:
print("❌ Some databases failed to create. Check errors above.")
sys.exit(1)
if __name__ == "__main__":
main()