P3.1 — Alembic wiring:
- alembic.ini: was hardcoded to Supabase env vars that never resolved.
Now uses canonical DATABASE_URL (Postgres).
- alembic/env.py: reads DATABASE_URL env var (with async->sync driver
conversion), falls back to alembic.ini default.
- alembic/versions/0001_baseline.py: captures core tables (tokens,
alerts, news_items, scan_reports, rag_findings, x402_receipts,
profiles, contract_audits, badges, chat_messages) that were previously
created via scattered CREATE TABLE IF NOT EXISTS across the codebase.
Uses IF NOT EXISTS for idempotent re-run.
- Documented: no ORM models, so autogenerate doesn't work. All
migrations must be hand-written: alembic revision -m ...
P4.1 — async correctness:
- oracle_manipulation_detector.py:1287 was inside async def main() with
time.sleep(30). Wrapped in async _monitor_loop() with asyncio.sleep().
- auto_healing.py:82 was a web endpoint that ran os.system('docker
restart rmi-redis') directly from a FastAPI request — security hole.
Replaced with audit-logged stub that requires manual operator action.
185 lines
No EOL
4.9 KiB
Python
185 lines
No EOL
4.9 KiB
Python
"""Baseline schema migration.
|
|
|
|
Captures the core tables that were created via scattered ``CREATE TABLE
|
|
IF NOT EXISTS`` statements across the codebase before Alembic existed.
|
|
This migration is idempotent (uses ``IF NOT EXISTS``) so re-running it
|
|
on a database that already has these tables is a no-op.
|
|
|
|
After this baseline, all schema changes MUST go through Alembic:
|
|
|
|
alembic revision -m "add_my_field"
|
|
# edit alembic/versions/00xx_add_my_field.py
|
|
alembic upgrade head
|
|
|
|
The codebase has no SQLAlchemy ORM models (uses raw SQL + Supabase),
|
|
so ``alembic revision --autogenerate`` cannot introspect. All
|
|
migrations must be written by hand.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from alembic import op
|
|
|
|
# revision identifiers, used by Alembic.
|
|
revision = "0001_baseline"
|
|
down_revision = None
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
"""Baseline: create the core tables that were previously scattered."""
|
|
# catalog schema (app/catalog/init_schema.py)
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS tokens (
|
|
address TEXT PRIMARY KEY,
|
|
chain TEXT NOT NULL,
|
|
name TEXT,
|
|
symbol TEXT,
|
|
decimals INT,
|
|
total_supply NUMERIC,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
)
|
|
"""
|
|
)
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS alerts (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
user_id TEXT NOT NULL,
|
|
alert_type TEXT NOT NULL,
|
|
severity TEXT,
|
|
payload JSONB,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
)
|
|
"""
|
|
)
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS news_items (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
source TEXT NOT NULL,
|
|
title TEXT,
|
|
url TEXT,
|
|
content TEXT,
|
|
sentiment NUMERIC,
|
|
published_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
)
|
|
"""
|
|
)
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS scan_reports (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
user_id TEXT,
|
|
target_address TEXT NOT NULL,
|
|
chain TEXT,
|
|
risk_score INT,
|
|
payload JSONB,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
)
|
|
"""
|
|
)
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS rag_findings (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
collection TEXT NOT NULL,
|
|
content TEXT,
|
|
metadata JSONB,
|
|
score NUMERIC,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
)
|
|
"""
|
|
)
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS x402_receipts (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
agent_id TEXT,
|
|
tool TEXT NOT NULL,
|
|
tx_hash TEXT,
|
|
amount_usdc NUMERIC,
|
|
paid_via_x402 TEXT,
|
|
paid_at TIMESTAMPTZ DEFAULT NOW()
|
|
)
|
|
"""
|
|
)
|
|
op.execute("CREATE INDEX IF NOT EXISTS idx_x402_paid_at ON x402_receipts(paid_at DESC)")
|
|
op.execute("CREATE INDEX IF NOT EXISTS idx_x402_tool ON x402_receipts(tool)")
|
|
op.execute("CREATE INDEX IF NOT EXISTS idx_x402_agent ON x402_receipts(agent_id)")
|
|
|
|
# app/db_client.py schema
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS profiles (
|
|
id TEXT PRIMARY KEY,
|
|
display_name TEXT,
|
|
email TEXT,
|
|
wallet_address TEXT,
|
|
wallet_chain TEXT,
|
|
tier TEXT DEFAULT 'FREE',
|
|
role TEXT DEFAULT 'USER',
|
|
xp INT DEFAULT 0,
|
|
level INT DEFAULT 1,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
)
|
|
"""
|
|
)
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS contract_audits (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
contract_address TEXT NOT NULL,
|
|
chain TEXT,
|
|
report JSONB,
|
|
risk_score INT,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
)
|
|
"""
|
|
)
|
|
|
|
# app/community_badges.py schema
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS badges (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT,
|
|
description TEXT,
|
|
emoji TEXT,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
)
|
|
"""
|
|
)
|
|
|
|
# app/routers/chat.py schema
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS chat_messages (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
user_id TEXT NOT NULL,
|
|
role TEXT,
|
|
content TEXT,
|
|
metadata JSONB,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
)
|
|
"""
|
|
)
|
|
|
|
|
|
def downgrade() -> None:
|
|
"""Drop baseline tables (DESTRUCTIVE)."""
|
|
for table in [
|
|
"chat_messages",
|
|
"badges",
|
|
"contract_audits",
|
|
"profiles",
|
|
"x402_receipts",
|
|
"rag_findings",
|
|
"scan_reports",
|
|
"news_items",
|
|
"alerts",
|
|
"tokens",
|
|
]:
|
|
op.execute(f"DROP TABLE IF EXISTS {table} CASCADE") |