feat: initial scaffold of osint_db (DuckDB source-of-truth service)
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
---
|
||||
name: osint_db
|
||||
lang: py
|
||||
domain: osint
|
||||
version: 0.1.0
|
||||
description: "Service FastAPI local (solo 127.0.0.1, puerto 8771) dueño único de la base DuckDB data/osint.duckdb: fuente de verdad estructurada del ecosistema OSINT. Indexa el vault de Obsidian osint (notes + persons/organizations/domains/cases/places con note_path), importa las maestras DAV de Xandikos (contacts, events), computa derivadas sin referencias a notas y renderiza tableros Markdown con bloques sentinel idempotentes dentro del vault."
|
||||
tags: [service, osint, duckdb, obsidian, dav, vault]
|
||||
uses_functions:
|
||||
- list_obsidian_notes_py_obsidian
|
||||
- read_obsidian_note_py_obsidian
|
||||
- update_obsidian_note_py_obsidian
|
||||
- create_obsidian_note_py_obsidian
|
||||
- slugify_obsidian_name_py_obsidian
|
||||
- dav_get_collection_py_infra
|
||||
- dav_list_calendars_py_infra
|
||||
- dav_collection_ctag_py_infra
|
||||
- pass_get_secret_py_infra
|
||||
- duckdb_query_readonly_py_infra
|
||||
- render_markdown_table_py_core
|
||||
- upsert_sentinel_block_py_core
|
||||
uses_types: []
|
||||
framework: "fastapi"
|
||||
entry_point: "server/main.py"
|
||||
dir_path: "projects/osint/apps/osint_db"
|
||||
repo_url: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/dataforge/osint_db"
|
||||
service:
|
||||
port: 8771
|
||||
health_endpoint: /api/health
|
||||
health_timeout_s: 3
|
||||
systemd_unit: null
|
||||
systemd_scope: null
|
||||
restart_policy: none
|
||||
runtime: manual
|
||||
pc_targets:
|
||||
- lucas-linux
|
||||
is_local_only: true
|
||||
e2e_checks:
|
||||
- id: tests
|
||||
cmd: ".venv/bin/python -m pytest tests -q"
|
||||
timeout_s: 120
|
||||
- id: vault_missing
|
||||
cmd: ".venv/bin/python server/main.py --vault /no/existe --port 0"
|
||||
expect_exit: 2
|
||||
timeout_s: 30
|
||||
---
|
||||
|
||||
## Qué es
|
||||
|
||||
Service del project `osint` que posee en exclusiva la base DuckDB
|
||||
`data/osint.duckdb`, la fuente de verdad estructurada del ecosistema OSINT
|
||||
(vault de Obsidian `~/Obsidian/osint` + servidor Xandikos CardDAV/CalDAV). Un
|
||||
plugin de Obsidian se construye en paralelo contra el contrato de esta API, por
|
||||
eso los endpoints de datos responden SIEMPRE HTTP 200 con `status: ok|error`
|
||||
en el body (el plugin parsea el body, no el código HTTP).
|
||||
|
||||
## Arquitectura de datos (3 categorías)
|
||||
|
||||
1. **Maestras con referencia a notas** (schema `main`): `notes` (índice
|
||||
completo del vault) + `persons`, `organizations`, `domains`, `cases`,
|
||||
`places` (fichas de nivel-1 de cada carpeta de entidades, excluyendo las
|
||||
notas con prefijo `_`). Cada una lleva `note_path`: el path relativo de la
|
||||
nota dentro del vault.
|
||||
2. **Maestras DAV** (schema `main`): `contacts` y `events` importados de
|
||||
Xandikos — fuente de verdad del lado agenda/calendario. `contacts.note_path`
|
||||
se enlaza contra `persons` matcheando por UID `osint-<slug>`, por el
|
||||
`dav_uid` extraído del campo `fuente` de la ficha, por teléfono normalizado
|
||||
o por email.
|
||||
3. **Derivadas** (schema `derived`): SOLO datos computados. **Regla dura:
|
||||
ninguna tabla de `derived` lleva columna que referencie notas** (`note_path`
|
||||
prohibido ahí; hay un test que lo verifica vía `information_schema`). Se
|
||||
reconstruyen completas (DROP + CREATE) en cada ingest:
|
||||
`derived.person_stats` (agregados por contexto/país/tag),
|
||||
`derived.event_monthly` (eventos por calendario y mes) y
|
||||
`derived.contact_link_quality` (contactos enlazados vs no, solo números).
|
||||
|
||||
Single-writer: SOLO este service escribe la DuckDB. La conexión de escritura se
|
||||
abre bajo demanda (migraciones, ingest, render) serializada con un lock de
|
||||
proceso y se cierra al terminar; las lecturas de `/api/query` abren su propia
|
||||
conexión `read_only` vía `duckdb_query_readonly`.
|
||||
|
||||
## Migraciones
|
||||
|
||||
`migrations/NNN_*.sql` numeradas, aditivas e idempotentes, aplicadas en orden
|
||||
al arrancar. La tabla `_migrations` registra las aplicadas (regla
|
||||
`db_migrations` adaptada a DuckDB).
|
||||
|
||||
## Arrancar
|
||||
|
||||
```bash
|
||||
cd projects/osint/apps/osint_db
|
||||
uv sync # primera vez: crea .venv
|
||||
.venv/bin/python server/main.py # defaults: vault ~/Obsidian/osint, puerto 8771
|
||||
.venv/bin/python server/main.py --vault ~/Obsidian/osint --db data/osint.duckdb --port 8771
|
||||
```
|
||||
|
||||
Health check: `curl http://127.0.0.1:8771/api/health`.
|
||||
|
||||
## Endpoints
|
||||
|
||||
| Método | Ruta | Qué hace |
|
||||
|---|---|---|
|
||||
| GET | `/api/health` | `{"status":"ok","db_path":"...","tables":N}` |
|
||||
| GET | `/api/tables` | inventario: schema, name, kind master/derived, row_count, columnas |
|
||||
| POST | `/api/query` | `{sql, params, max_rows}` → respuesta exacta de `duckdb_query_readonly` (solo lectura) |
|
||||
| GET | `/api/queries` | catálogo de queries con nombre (`server/named_queries.py`) |
|
||||
| POST | `/api/query/named` | `{name, max_rows}` → misma shape que `/api/query` |
|
||||
| POST | `/api/ingest/vault` | escanea el vault completo y reconstruye notes + entidades + derivadas |
|
||||
| POST | `/api/ingest/dav` | baja Xandikos (CardDAV + cada calendario CalDAV), reconstruye contacts/events, enlaza y reconstruye derivadas |
|
||||
| POST | `/api/render/note` | `{note_path, block_id, sql\|query, title?}` → tabla Markdown upsertada como bloque sentinel `osintdb` en la nota (la crea si no existe) |
|
||||
|
||||
Queries con nombre incluidas: `personas_por_contexto`, `personas_recientes`,
|
||||
`eventos_proximos`, `contactos_sin_nota`, `stats_personas`,
|
||||
`calidad_enlace_contactos`, `eventos_por_mes`.
|
||||
|
||||
## Configuración
|
||||
|
||||
`server/config.py`: vault (`~/Obsidian/osint`), db (`data/osint.duckdb`
|
||||
relativa a la app), DAV base/colecciones de Xandikos (los mismos valores que
|
||||
`projects/osint/tools/sync_dav_to_osint.py`) y puerto 8771. Overrides por CLI:
|
||||
`--vault`, `--db`, `--port`. La credencial DAV se resuelve SIEMPRE con
|
||||
`pass_get_secret("dav/xandikos-enmanuel")`, nunca hardcodeada.
|
||||
|
||||
## Seguridad
|
||||
|
||||
- El vault y la base contienen datos personales sensibles: el server escucha
|
||||
solo en `127.0.0.1` y `runtime: manual` (no se despliega a VPS,
|
||||
`is_local_only: true`).
|
||||
- `/api/query` es estrictamente read-only (conexión `read_only` de DuckDB).
|
||||
- `/api/render/note` valida que el path destino no escapa del vault (realpath
|
||||
bajo el realpath del vault).
|
||||
- Vault inexistente al arrancar → error claro en stderr + exit 2.
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
cd projects/osint/apps/osint_db
|
||||
.venv/bin/python -m pytest tests -q
|
||||
```
|
||||
|
||||
Vault temporal + DuckDB temporal, sin red: migraciones idempotentes, ingest del
|
||||
vault con fixture (conteos, exclusión de sub-notas y `_`), extracción de
|
||||
`dav_uid` desde `fuente`, `/api/query` ok/error/solo-lectura (siempre HTTP
|
||||
200), catálogo y queries con nombre, inventario de tablas, regla dura derivadas
|
||||
sin `note_path`, enlace contacto→ficha por teléfono y render de nota con bloque
|
||||
sentinel idempotente + validación de inputs y path traversal.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- DuckDB bloquea el archivo a un escritor exclusivo: mientras un ingest está
|
||||
escribiendo, una lectura `read_only` concurrente puede devolver
|
||||
`{"status":"error"}` por conflicto de lock. El cliente reintenta.
|
||||
- `/api/ingest/dav` requiere `pass` desbloqueado (gpg-agent); si gpg está
|
||||
bloqueado devuelve `{"status":"error"}` con el detalle, sin crash.
|
||||
- Las notas con prefijo `_` (`_indice.md`, `_plantilla.md`) y las sub-notas de
|
||||
documento (`personas/<slug>/<doc>.md`) entran en `notes` pero NO cuentan como
|
||||
entidades.
|
||||
Reference in New Issue
Block a user