2026-05-18 01:44:36 +00:00
|
|
|
# Disclosure Bureau — full deployment stack.
|
|
|
|
|
# Routed via the host's existing plegal-traefik (network: traefik-public).
|
|
|
|
|
# Internal services share the disclosure-internal network and are NOT exposed
|
|
|
|
|
# to the host. Public services (web, kong, studio, search) get Traefik labels.
|
|
|
|
|
|
|
|
|
|
name: disclosure
|
|
|
|
|
|
|
|
|
|
networks:
|
|
|
|
|
internal:
|
|
|
|
|
name: disclosure-internal
|
|
|
|
|
driver: bridge
|
|
|
|
|
traefik:
|
|
|
|
|
name: traefik-public
|
|
|
|
|
external: true
|
|
|
|
|
|
|
|
|
|
volumes:
|
|
|
|
|
db-data:
|
|
|
|
|
storage-data:
|
|
|
|
|
meili-data:
|
|
|
|
|
hf-cache:
|
W0+W1+W1.2: security hardening, observability, autocomplete, glitchtip, forgejo CI
W0 — security hardening (5 fixes verified live on disclosure.top)
- middleware: gate /api/admin/* same as /admin/* (F1)
- imgproxy: tighten LOCAL_FILESYSTEM_ROOT from / to /var/lib/storage (F2)
- studio: real basic-auth label (bcrypt hash, middleware reference) (F3)
- relations: ENABLE ROW LEVEL SECURITY + public SELECT policy (F4)
- migration 0003: fold is_searchable + hybrid_search update into canonical (TD#2)
W1 — observability + resilience + autocomplete
- studio: HOSTNAME=0.0.0.0 so Next.js binds on loopback for healthcheck
- compose: PG_POOL_MAX=20, CLAUDE_CODE_OAUTH_TOKEN gated by separate env
- claude-code.ts: subprocess timeout configurable (CLAUDE_CODE_TIMEOUT_MS)
- openrouter.ts: retry with exponential backoff + Retry-After + in-memory
circuit breaker (promotes FALLBACK after CB_THRESHOLD failures)
- lib/logger.ts: pino logger (NDJSON prod / pretty dev) + withRequest helper
- middleware: mints correlation_id, stamps x-correlation-id response header,
emits structured http_request log per /api/* call
- messages/route.ts: switch to structured logger
- 60_meili_index.py: push documents + chunks into Meilisearch
- /api/search/autocomplete: parallel meili search (docs + chunks), 5-8ms p50
- search-autocomplete.tsx: debounced dropdown wired into search-panel
W1.2 — Glitchtip + Forgejo self-hosted
- compose: glitchtip-redis + glitchtip-web + glitchtip-worker (v4.2)
- compose: forgejo + forgejo-runner (server v9, runner v6) with group_add=988
- @sentry/nextjs SDK wired (instrumentation.ts + sentry.{client,server}.config.ts)
- /api/admin/throw smoke endpoint (gated by W0-F1 middleware)
- Synthetic event ingestion verified at glitchtip.disclosure.top
- forgejo.disclosure.top up, repo discadmin/disclosure-bureau created,
runner registered (labels: ubuntu-latest, docker)
- .forgejo/workflows/ci.yml: typecheck + lint + build + npm audit + python
syntax + compose validation
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 21:18:42 +00:00
|
|
|
glitchtip-redis-data:
|
|
|
|
|
glitchtip-uploads:
|
|
|
|
|
forgejo-data:
|
|
|
|
|
forgejo-runner-config:
|
2026-05-18 01:44:36 +00:00
|
|
|
|
|
|
|
|
services:
|
|
|
|
|
# ─── Database ─────────────────────────────────────────────────────────────
|
|
|
|
|
db:
|
|
|
|
|
container_name: disclosure-db
|
|
|
|
|
image: supabase/postgres:15.8.1.060
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
networks: [internal]
|
|
|
|
|
healthcheck:
|
|
|
|
|
test: ["CMD-SHELL", "pg_isready -U postgres -h localhost"]
|
|
|
|
|
interval: 10s
|
|
|
|
|
timeout: 5s
|
|
|
|
|
retries: 12
|
|
|
|
|
environment:
|
|
|
|
|
POSTGRES_HOST: /var/run/postgresql
|
|
|
|
|
POSTGRES_PORT: 5432
|
|
|
|
|
POSTGRES_DB: postgres
|
|
|
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
|
|
|
|
PGPASSWORD: ${POSTGRES_PASSWORD}
|
|
|
|
|
JWT_SECRET: ${JWT_SECRET}
|
|
|
|
|
JWT_EXP: 3600
|
|
|
|
|
POSTGRES_INITDB_ARGS: "--data-checksums"
|
|
|
|
|
command:
|
|
|
|
|
- postgres
|
|
|
|
|
- -c
|
|
|
|
|
- shared_buffers=${POSTGRES_SHARED_BUFFERS:-384MB}
|
|
|
|
|
- -c
|
|
|
|
|
- work_mem=${POSTGRES_WORK_MEM:-12MB}
|
|
|
|
|
- -c
|
|
|
|
|
- maintenance_work_mem=${POSTGRES_MAINTENANCE_WORK_MEM:-96MB}
|
|
|
|
|
- -c
|
|
|
|
|
- max_connections=${POSTGRES_MAX_CONNECTIONS:-80}
|
|
|
|
|
volumes:
|
|
|
|
|
- db-data:/var/lib/postgresql/data
|
|
|
|
|
|
|
|
|
|
# ─── Auth (GoTrue) ────────────────────────────────────────────────────────
|
|
|
|
|
auth:
|
|
|
|
|
container_name: disclosure-auth
|
|
|
|
|
image: supabase/gotrue:v2.170.0
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
networks: [internal]
|
|
|
|
|
depends_on:
|
|
|
|
|
db: { condition: service_healthy }
|
|
|
|
|
environment:
|
|
|
|
|
GOTRUE_API_HOST: 0.0.0.0
|
|
|
|
|
GOTRUE_API_PORT: 9999
|
|
|
|
|
API_EXTERNAL_URL: https://${DOMAIN_API}
|
|
|
|
|
GOTRUE_DB_DRIVER: postgres
|
|
|
|
|
GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD}@db:5432/postgres?search_path=auth
|
|
|
|
|
GOTRUE_SITE_URL: https://${DOMAIN_MAIN}
|
ship: synthesize 158 entities, AG-UI artifacts, chat persistence, auth flow
Fase 3 onda 2 — entity synthesis at scale:
- scripts/synthesize/20_entity_summary.py: queries DB for entities with
total_mentions ≥ threshold + top-K verbatim chunk snippets via
entity_mentions JOIN, prompts Sonnet (Holmes-Watson voice, bilingual),
writes narrative_summary EN+PT-BR + summary_status=synthesized.
Ran on 187 candidates (mentions ≥ 20) → 158 OK · 1 err · 29 skipped (no
snippets). Combined with anchor curation: 20 curated + 158 synthesized
= 178 entities with real narrative (vs 0 a day ago).
Fase 4 — chat with typed artifacts + persistence:
- lib/chat/agui.ts: AG-UI v1 typed Artifact union (citation, crop_image,
entity_card, evidence_card, hypothesis_card, case_card, navigation_offer)
alongside the existing event types.
- lib/chat/tools.ts + openrouter.ts: hybrid_search emits up to 6
citation + crop_image artifacts per query. Provider collects them and
returns in done.artifacts so the route can persist.
- api/sessions/[id]/messages: persist artifacts to messages.citations.
- components/chat-bubble.tsx: ArtifactCard renders inline cards (citation,
crop_image, entity_card, navigation_offer) for streamed and persisted
messages. activeId now persisted in localStorage so navigation between
pages keeps the same conversation. New sessions are lazy (only when user
has zero). loadMessages hydrates tools + artifacts from server. CRUD UI:
rename (✎) + archive (🗑) buttons per session in the list.
Home search:
- doc-list-filters: input now fires hybrid_search (rerank=0 for speed)
in parallel with the local title filter; chunk hits render above the doc
grid with snippet + score + classification.
- api/search/hybrid: accept ?rerank=0 to skip the cross-encoder (1.3s vs 60s).
Auth flow:
- infra: SMTP_HOST=mail.spacemail.com:587 + DMARC published; mail now lands
in inbox. GOTRUE_MAILER_AUTOCONFIRM=false (real email verification).
- kong.yml: proxy /auth/callback on api.disclosure.top → web:3000 so PKCE
email links don't 404 at the gateway.
- web/app/auth/callback: handle both ?code= (OAuth) and ?token=&type=
(PKCE); redirect to the public site host before verifyOtp so the session
cookie lands on the right domain.
Audit deliverables:
- .nirvana/outputs/disclosure-bureau/.../systems-atelier/: 5 docs (code
analysis, tech debt, discovery brief, system arch, 5 ADRs) authored by
sa-principal that produced this roadmap. Kept in-tree for traceability.
2026-05-18 06:52:59 +00:00
|
|
|
# Explicit external URL so confirmation links land on the public site
|
|
|
|
|
# (Next.js /auth/callback), not on the Kong gateway host.
|
|
|
|
|
GOTRUE_API_EXTERNAL_URL: https://${DOMAIN_MAIN}
|
2026-05-18 01:44:36 +00:00
|
|
|
GOTRUE_URI_ALLOW_LIST: https://${DOMAIN_MAIN},https://www.${DOMAIN_MAIN}
|
|
|
|
|
GOTRUE_DISABLE_SIGNUP: "false"
|
|
|
|
|
GOTRUE_JWT_ADMIN_ROLES: service_role
|
|
|
|
|
GOTRUE_JWT_AUD: authenticated
|
|
|
|
|
GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
|
|
|
|
|
GOTRUE_JWT_EXP: 3600
|
|
|
|
|
GOTRUE_JWT_SECRET: ${JWT_SECRET}
|
|
|
|
|
GOTRUE_EXTERNAL_EMAIL_ENABLED: "true"
|
phase-0: kill stubs, ship 20 curated anchor events, configure SMTP
- scripts/03-dedup-entities.py: stop emitting placeholder narrative ("Stub. Will
be enriched in Phase 7"); write summary_status=none + null fields instead.
- scripts/maintain/41_strip_stubs.py: idempotent migration that cleaned the
22,096 entity .md files (now zero stub strings in wiki/).
- scripts/synthesize/01_anchor_events.py: curated 20 anchor UAP events
(Roswell, Nimitz Tic-Tac, Phoenix Lights, Operação Prato, AATIP, etc.) with
bilingual Holmes-Watson narrative via claude -p --model sonnet
(CLAUDE_CODE_OAUTH_TOKEN). All summary_status=curated, confidence=high.
- web/api/timeline + timeline-view: filter narrative-less events by default,
render "curado" badge for hand-vetted ones, drop the date display alone.
- CLAUDE-schema-full.md: document the summary_status enum and the four states.
- docker-compose.yml: SMTP_HOST=mail.spacemail.com configured;
GOTRUE_MAILER_AUTOCONFIRM flipped to false (real email confirmation working).
- .nirvana/outputs/.../systems-atelier/: 5 deliverables of the architecture
audit that produced this roadmap.
2026-05-18 03:44:17 +00:00
|
|
|
# SMTP configured (Spacemail) → email confirmation required for signups.
|
|
|
|
|
GOTRUE_MAILER_AUTOCONFIRM: "false"
|
2026-05-18 01:44:36 +00:00
|
|
|
GOTRUE_MAILER_OTP_EXP: 3600
|
|
|
|
|
GOTRUE_SMTP_HOST: ${SMTP_HOST}
|
|
|
|
|
GOTRUE_SMTP_PORT: ${SMTP_PORT}
|
|
|
|
|
GOTRUE_SMTP_USER: ${SMTP_USER}
|
|
|
|
|
GOTRUE_SMTP_PASS: ${SMTP_PASS}
|
|
|
|
|
GOTRUE_SMTP_ADMIN_EMAIL: ${SMTP_FROM}
|
|
|
|
|
GOTRUE_SMTP_SENDER_NAME: ${SMTP_FROM_NAME}
|
|
|
|
|
GOTRUE_MAILER_URLPATHS_INVITE: /auth/callback
|
|
|
|
|
GOTRUE_MAILER_URLPATHS_CONFIRMATION: /auth/callback
|
|
|
|
|
GOTRUE_MAILER_URLPATHS_RECOVERY: /auth/callback
|
|
|
|
|
GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: /auth/callback
|
|
|
|
|
|
|
|
|
|
# ─── PostgREST ────────────────────────────────────────────────────────────
|
|
|
|
|
rest:
|
|
|
|
|
container_name: disclosure-rest
|
|
|
|
|
image: postgrest/postgrest:v12.2.8
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
networks: [internal]
|
|
|
|
|
depends_on:
|
|
|
|
|
db: { condition: service_healthy }
|
|
|
|
|
environment:
|
|
|
|
|
PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@db:5432/postgres
|
|
|
|
|
PGRST_DB_SCHEMAS: public,storage
|
|
|
|
|
PGRST_DB_ANON_ROLE: anon
|
|
|
|
|
PGRST_JWT_SECRET: ${JWT_SECRET}
|
|
|
|
|
PGRST_DB_USE_LEGACY_GUCS: "false"
|
|
|
|
|
PGRST_APP_SETTINGS_JWT_SECRET: ${JWT_SECRET}
|
|
|
|
|
PGRST_APP_SETTINGS_JWT_EXP: 3600
|
|
|
|
|
|
|
|
|
|
# ─── Realtime ─────────────────────────────────────────────────────────────
|
|
|
|
|
realtime:
|
|
|
|
|
container_name: disclosure-realtime
|
|
|
|
|
image: supabase/realtime:v2.30.34
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
networks: [internal]
|
|
|
|
|
depends_on:
|
|
|
|
|
db: { condition: service_healthy }
|
|
|
|
|
environment:
|
|
|
|
|
PORT: 4000
|
|
|
|
|
DB_HOST: db
|
|
|
|
|
DB_PORT: 5432
|
|
|
|
|
DB_USER: supabase_admin
|
|
|
|
|
DB_PASSWORD: ${POSTGRES_PASSWORD}
|
|
|
|
|
DB_NAME: postgres
|
|
|
|
|
DB_ENC_KEY: ${VAULT_ENC_KEY}
|
|
|
|
|
API_JWT_SECRET: ${JWT_SECRET}
|
|
|
|
|
SECRET_KEY_BASE: ${SECRET_KEY_BASE}
|
|
|
|
|
ERL_AFLAGS: -proto_dist inet_tcp
|
|
|
|
|
DNS_NODES: "''"
|
|
|
|
|
RLIMIT_NOFILE: "10000"
|
|
|
|
|
APP_NAME: realtime
|
|
|
|
|
SEED_SELF_HOST: "true"
|
|
|
|
|
RUN_JANITOR: "true"
|
|
|
|
|
|
|
|
|
|
# ─── Storage ──────────────────────────────────────────────────────────────
|
|
|
|
|
storage:
|
|
|
|
|
container_name: disclosure-storage
|
|
|
|
|
image: supabase/storage-api:v1.14.3
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
networks: [internal]
|
|
|
|
|
depends_on:
|
|
|
|
|
db: { condition: service_healthy }
|
|
|
|
|
rest: { condition: service_started }
|
|
|
|
|
imgproxy: { condition: service_started }
|
|
|
|
|
environment:
|
|
|
|
|
ANON_KEY: ${ANON_KEY}
|
|
|
|
|
SERVICE_KEY: ${SERVICE_ROLE_KEY}
|
|
|
|
|
POSTGREST_URL: http://rest:3000
|
|
|
|
|
PGRST_JWT_SECRET: ${JWT_SECRET}
|
|
|
|
|
DATABASE_URL: postgres://supabase_storage_admin:${POSTGRES_PASSWORD}@db:5432/postgres
|
|
|
|
|
FILE_SIZE_LIMIT: 52428800
|
|
|
|
|
STORAGE_BACKEND: file
|
|
|
|
|
FILE_STORAGE_BACKEND_PATH: /var/lib/storage
|
|
|
|
|
TENANT_ID: stub
|
|
|
|
|
REGION: stub
|
|
|
|
|
GLOBAL_S3_BUCKET: stub
|
|
|
|
|
ENABLE_IMAGE_TRANSFORMATION: "true"
|
|
|
|
|
IMGPROXY_URL: http://imgproxy:5001
|
|
|
|
|
volumes:
|
|
|
|
|
- storage-data:/var/lib/storage
|
|
|
|
|
|
|
|
|
|
imgproxy:
|
|
|
|
|
container_name: disclosure-imgproxy
|
|
|
|
|
image: darthsim/imgproxy:v3.8.0
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
networks: [internal]
|
|
|
|
|
environment:
|
|
|
|
|
IMGPROXY_BIND: ":5001"
|
W0+W1+W1.2: security hardening, observability, autocomplete, glitchtip, forgejo CI
W0 — security hardening (5 fixes verified live on disclosure.top)
- middleware: gate /api/admin/* same as /admin/* (F1)
- imgproxy: tighten LOCAL_FILESYSTEM_ROOT from / to /var/lib/storage (F2)
- studio: real basic-auth label (bcrypt hash, middleware reference) (F3)
- relations: ENABLE ROW LEVEL SECURITY + public SELECT policy (F4)
- migration 0003: fold is_searchable + hybrid_search update into canonical (TD#2)
W1 — observability + resilience + autocomplete
- studio: HOSTNAME=0.0.0.0 so Next.js binds on loopback for healthcheck
- compose: PG_POOL_MAX=20, CLAUDE_CODE_OAUTH_TOKEN gated by separate env
- claude-code.ts: subprocess timeout configurable (CLAUDE_CODE_TIMEOUT_MS)
- openrouter.ts: retry with exponential backoff + Retry-After + in-memory
circuit breaker (promotes FALLBACK after CB_THRESHOLD failures)
- lib/logger.ts: pino logger (NDJSON prod / pretty dev) + withRequest helper
- middleware: mints correlation_id, stamps x-correlation-id response header,
emits structured http_request log per /api/* call
- messages/route.ts: switch to structured logger
- 60_meili_index.py: push documents + chunks into Meilisearch
- /api/search/autocomplete: parallel meili search (docs + chunks), 5-8ms p50
- search-autocomplete.tsx: debounced dropdown wired into search-panel
W1.2 — Glitchtip + Forgejo self-hosted
- compose: glitchtip-redis + glitchtip-web + glitchtip-worker (v4.2)
- compose: forgejo + forgejo-runner (server v9, runner v6) with group_add=988
- @sentry/nextjs SDK wired (instrumentation.ts + sentry.{client,server}.config.ts)
- /api/admin/throw smoke endpoint (gated by W0-F1 middleware)
- Synthetic event ingestion verified at glitchtip.disclosure.top
- forgejo.disclosure.top up, repo discadmin/disclosure-bureau created,
runner registered (labels: ubuntu-latest, docker)
- .forgejo/workflows/ci.yml: typecheck + lint + build + npm audit + python
syntax + compose validation
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 21:18:42 +00:00
|
|
|
# W0-F2: tighten filesystem root from "/" (whole VPS) to the Storage
|
|
|
|
|
# backend mount only. Imgproxy never reads outside Storage objects.
|
|
|
|
|
IMGPROXY_LOCAL_FILESYSTEM_ROOT: /var/lib/storage
|
2026-05-18 01:44:36 +00:00
|
|
|
IMGPROXY_USE_ETAG: "true"
|
|
|
|
|
IMGPROXY_ENABLE_WEBP_DETECTION: "true"
|
|
|
|
|
volumes:
|
|
|
|
|
- storage-data:/var/lib/storage
|
|
|
|
|
|
|
|
|
|
# ─── pg-meta + Studio ─────────────────────────────────────────────────────
|
|
|
|
|
meta:
|
|
|
|
|
container_name: disclosure-meta
|
|
|
|
|
image: supabase/postgres-meta:v0.83.2
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
networks: [internal]
|
|
|
|
|
depends_on:
|
|
|
|
|
db: { condition: service_healthy }
|
|
|
|
|
environment:
|
|
|
|
|
PG_META_PORT: 8080
|
|
|
|
|
PG_META_DB_HOST: db
|
|
|
|
|
PG_META_DB_PORT: 5432
|
|
|
|
|
PG_META_DB_NAME: postgres
|
|
|
|
|
PG_META_DB_USER: supabase_admin
|
|
|
|
|
PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD}
|
|
|
|
|
|
|
|
|
|
studio:
|
|
|
|
|
container_name: disclosure-studio
|
|
|
|
|
image: supabase/studio:20241202-71e5240
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
networks: [internal, traefik]
|
|
|
|
|
depends_on:
|
|
|
|
|
meta: { condition: service_started }
|
|
|
|
|
environment:
|
W0+W1+W1.2: security hardening, observability, autocomplete, glitchtip, forgejo CI
W0 — security hardening (5 fixes verified live on disclosure.top)
- middleware: gate /api/admin/* same as /admin/* (F1)
- imgproxy: tighten LOCAL_FILESYSTEM_ROOT from / to /var/lib/storage (F2)
- studio: real basic-auth label (bcrypt hash, middleware reference) (F3)
- relations: ENABLE ROW LEVEL SECURITY + public SELECT policy (F4)
- migration 0003: fold is_searchable + hybrid_search update into canonical (TD#2)
W1 — observability + resilience + autocomplete
- studio: HOSTNAME=0.0.0.0 so Next.js binds on loopback for healthcheck
- compose: PG_POOL_MAX=20, CLAUDE_CODE_OAUTH_TOKEN gated by separate env
- claude-code.ts: subprocess timeout configurable (CLAUDE_CODE_TIMEOUT_MS)
- openrouter.ts: retry with exponential backoff + Retry-After + in-memory
circuit breaker (promotes FALLBACK after CB_THRESHOLD failures)
- lib/logger.ts: pino logger (NDJSON prod / pretty dev) + withRequest helper
- middleware: mints correlation_id, stamps x-correlation-id response header,
emits structured http_request log per /api/* call
- messages/route.ts: switch to structured logger
- 60_meili_index.py: push documents + chunks into Meilisearch
- /api/search/autocomplete: parallel meili search (docs + chunks), 5-8ms p50
- search-autocomplete.tsx: debounced dropdown wired into search-panel
W1.2 — Glitchtip + Forgejo self-hosted
- compose: glitchtip-redis + glitchtip-web + glitchtip-worker (v4.2)
- compose: forgejo + forgejo-runner (server v9, runner v6) with group_add=988
- @sentry/nextjs SDK wired (instrumentation.ts + sentry.{client,server}.config.ts)
- /api/admin/throw smoke endpoint (gated by W0-F1 middleware)
- Synthetic event ingestion verified at glitchtip.disclosure.top
- forgejo.disclosure.top up, repo discadmin/disclosure-bureau created,
runner registered (labels: ubuntu-latest, docker)
- .forgejo/workflows/ci.yml: typecheck + lint + build + npm audit + python
syntax + compose validation
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 21:18:42 +00:00
|
|
|
# W1: Next.js standalone server binds to the container hostname by
|
|
|
|
|
# default, leaving 127.0.0.1 unreachable — the Docker healthcheck
|
|
|
|
|
# (fetch 127.0.0.1:3000/api/profile) then loops on ECONNREFUSED and
|
|
|
|
|
# the service never goes healthy. HOSTNAME=0.0.0.0 forces it to bind
|
|
|
|
|
# on all interfaces so both the loopback and the docker IP respond.
|
|
|
|
|
HOSTNAME: 0.0.0.0
|
2026-05-18 01:44:36 +00:00
|
|
|
STUDIO_PG_META_URL: http://meta:8080
|
|
|
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
|
|
|
|
DEFAULT_ORGANIZATION_NAME: "Disclosure Bureau"
|
|
|
|
|
DEFAULT_PROJECT_NAME: "disclosure"
|
|
|
|
|
SUPABASE_URL: http://kong:8000
|
|
|
|
|
SUPABASE_PUBLIC_URL: https://${DOMAIN_API}
|
|
|
|
|
SUPABASE_ANON_KEY: ${ANON_KEY}
|
|
|
|
|
SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
|
|
|
|
|
AUTH_JWT_SECRET: ${JWT_SECRET}
|
|
|
|
|
DASHBOARD_USERNAME: ${DASHBOARD_USERNAME}
|
|
|
|
|
DASHBOARD_PASSWORD: ${DASHBOARD_PASSWORD}
|
|
|
|
|
labels:
|
|
|
|
|
- traefik.enable=true
|
|
|
|
|
- traefik.docker.network=traefik-public
|
|
|
|
|
- traefik.http.routers.disclosure-studio.rule=Host(`${DOMAIN_STUDIO}`)
|
|
|
|
|
- traefik.http.routers.disclosure-studio.entrypoints=websecure
|
|
|
|
|
- traefik.http.routers.disclosure-studio.tls=true
|
|
|
|
|
- traefik.http.routers.disclosure-studio.tls.certresolver=letsencrypt
|
|
|
|
|
- traefik.http.services.disclosure-studio.loadbalancer.server.port=3000
|
W0+W1+W1.2: security hardening, observability, autocomplete, glitchtip, forgejo CI
W0 — security hardening (5 fixes verified live on disclosure.top)
- middleware: gate /api/admin/* same as /admin/* (F1)
- imgproxy: tighten LOCAL_FILESYSTEM_ROOT from / to /var/lib/storage (F2)
- studio: real basic-auth label (bcrypt hash, middleware reference) (F3)
- relations: ENABLE ROW LEVEL SECURITY + public SELECT policy (F4)
- migration 0003: fold is_searchable + hybrid_search update into canonical (TD#2)
W1 — observability + resilience + autocomplete
- studio: HOSTNAME=0.0.0.0 so Next.js binds on loopback for healthcheck
- compose: PG_POOL_MAX=20, CLAUDE_CODE_OAUTH_TOKEN gated by separate env
- claude-code.ts: subprocess timeout configurable (CLAUDE_CODE_TIMEOUT_MS)
- openrouter.ts: retry with exponential backoff + Retry-After + in-memory
circuit breaker (promotes FALLBACK after CB_THRESHOLD failures)
- lib/logger.ts: pino logger (NDJSON prod / pretty dev) + withRequest helper
- middleware: mints correlation_id, stamps x-correlation-id response header,
emits structured http_request log per /api/* call
- messages/route.ts: switch to structured logger
- 60_meili_index.py: push documents + chunks into Meilisearch
- /api/search/autocomplete: parallel meili search (docs + chunks), 5-8ms p50
- search-autocomplete.tsx: debounced dropdown wired into search-panel
W1.2 — Glitchtip + Forgejo self-hosted
- compose: glitchtip-redis + glitchtip-web + glitchtip-worker (v4.2)
- compose: forgejo + forgejo-runner (server v9, runner v6) with group_add=988
- @sentry/nextjs SDK wired (instrumentation.ts + sentry.{client,server}.config.ts)
- /api/admin/throw smoke endpoint (gated by W0-F1 middleware)
- Synthetic event ingestion verified at glitchtip.disclosure.top
- forgejo.disclosure.top up, repo discadmin/disclosure-bureau created,
runner registered (labels: ubuntu-latest, docker)
- .forgejo/workflows/ci.yml: typecheck + lint + build + npm audit + python
syntax + compose validation
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 21:18:42 +00:00
|
|
|
# W0-F3: real basic auth (was effectively disabled with usersfile=/dev/null).
|
|
|
|
|
# The user/password is DASHBOARD_USERNAME / DASHBOARD_PASSWORD from .env;
|
|
|
|
|
# the bcrypt hash below was generated with $$ doubled for compose escaping.
|
|
|
|
|
# Rotate by regenerating: htpasswd -nbB <user> <pass> (then double every $).
|
|
|
|
|
- traefik.http.middlewares.disclosure-studio-auth.basicauth.users=admin:$$2b$$05$$tFLAMGNWX7xDbVyQ/O0G1.ruLwm3Le1.ErgdUTB9IYeJeH2FHd4ha
|
|
|
|
|
- traefik.http.routers.disclosure-studio.middlewares=disclosure-studio-auth@docker
|
2026-05-18 01:44:36 +00:00
|
|
|
|
|
|
|
|
# ─── Kong API gateway ─────────────────────────────────────────────────────
|
|
|
|
|
kong:
|
|
|
|
|
container_name: disclosure-kong
|
|
|
|
|
image: kong:2.8.1
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
networks: [internal, traefik]
|
|
|
|
|
depends_on:
|
|
|
|
|
auth: { condition: service_started }
|
|
|
|
|
rest: { condition: service_started }
|
|
|
|
|
realtime: { condition: service_started }
|
|
|
|
|
storage: { condition: service_started }
|
|
|
|
|
environment:
|
|
|
|
|
KONG_DATABASE: "off"
|
|
|
|
|
# Read rendered config (envsubst happens at startup, see entrypoint below)
|
|
|
|
|
KONG_DECLARATIVE_CONFIG: /tmp/kong.yml
|
|
|
|
|
KONG_DNS_ORDER: LAST,A,CNAME
|
|
|
|
|
KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth
|
|
|
|
|
KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k
|
|
|
|
|
KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k
|
|
|
|
|
SUPABASE_ANON_KEY: ${ANON_KEY}
|
|
|
|
|
SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
|
|
|
|
|
DASHBOARD_USERNAME: ${DASHBOARD_USERNAME}
|
|
|
|
|
DASHBOARD_PASSWORD: ${DASHBOARD_PASSWORD}
|
|
|
|
|
# Kong declarative config does NOT do env substitution by itself. We
|
|
|
|
|
# render the template into /tmp/kong.yml at container start so the JWT
|
|
|
|
|
# keys land literally in the config Kong actually loads.
|
|
|
|
|
user: root
|
|
|
|
|
entrypoint:
|
|
|
|
|
- /bin/sh
|
|
|
|
|
- -c
|
|
|
|
|
- |
|
|
|
|
|
apk add --no-cache gettext >/dev/null 2>&1 || true
|
|
|
|
|
envsubst < /usr/local/kong/kong.yml.tmpl > /tmp/kong.yml
|
|
|
|
|
chown kong:kong /tmp/kong.yml
|
|
|
|
|
exec /docker-entrypoint.sh kong docker-start
|
|
|
|
|
volumes:
|
|
|
|
|
- ./kong.yml:/usr/local/kong/kong.yml.tmpl:ro
|
|
|
|
|
labels:
|
|
|
|
|
- traefik.enable=true
|
|
|
|
|
- traefik.docker.network=traefik-public
|
|
|
|
|
- traefik.http.routers.disclosure-api.rule=Host(`${DOMAIN_API}`)
|
|
|
|
|
- traefik.http.routers.disclosure-api.entrypoints=websecure
|
|
|
|
|
- traefik.http.routers.disclosure-api.tls=true
|
|
|
|
|
- traefik.http.routers.disclosure-api.tls.certresolver=letsencrypt
|
|
|
|
|
- traefik.http.services.disclosure-api.loadbalancer.server.port=8000
|
|
|
|
|
|
|
|
|
|
# ─── Meilisearch ──────────────────────────────────────────────────────────
|
|
|
|
|
meilisearch:
|
|
|
|
|
container_name: disclosure-meili
|
|
|
|
|
image: getmeili/meilisearch:v1.10
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
networks: [internal, traefik]
|
|
|
|
|
environment:
|
|
|
|
|
MEILI_MASTER_KEY: ${MEILI_MASTER_KEY}
|
|
|
|
|
MEILI_NO_ANALYTICS: "true"
|
|
|
|
|
MEILI_ENV: production
|
|
|
|
|
MEILI_MAX_INDEXING_MEMORY: ${MEILI_MAX_INDEXING_MEMORY:-512MB}
|
|
|
|
|
volumes:
|
|
|
|
|
- meili-data:/meili_data
|
|
|
|
|
labels:
|
|
|
|
|
- traefik.enable=true
|
|
|
|
|
- traefik.docker.network=traefik-public
|
|
|
|
|
- traefik.http.routers.disclosure-search.rule=Host(`${DOMAIN_SEARCH}`)
|
|
|
|
|
- traefik.http.routers.disclosure-search.entrypoints=websecure
|
|
|
|
|
- traefik.http.routers.disclosure-search.tls=true
|
|
|
|
|
- traefik.http.routers.disclosure-search.tls.certresolver=letsencrypt
|
|
|
|
|
- traefik.http.services.disclosure-search.loadbalancer.server.port=7700
|
|
|
|
|
|
|
|
|
|
# ─── Next.js web (Disclosure Bureau frontend) ─────────────────────────────
|
|
|
|
|
web:
|
|
|
|
|
container_name: disclosure-web
|
|
|
|
|
build:
|
|
|
|
|
context: /data/disclosure/web # rsynced from laptop, see scripts/sync-data.sh
|
|
|
|
|
dockerfile: Dockerfile
|
|
|
|
|
args:
|
|
|
|
|
NEXT_PUBLIC_SUPABASE_URL: https://${DOMAIN_API}
|
|
|
|
|
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${ANON_KEY}
|
|
|
|
|
NEXT_PUBLIC_SITE_URL: https://${DOMAIN_MAIN}
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
networks: [internal, traefik]
|
|
|
|
|
depends_on:
|
|
|
|
|
kong: { condition: service_started }
|
|
|
|
|
environment:
|
|
|
|
|
NODE_ENV: production
|
|
|
|
|
NODE_OPTIONS: ${NEXT_NODE_OPTIONS:---max-old-space-size=768}
|
|
|
|
|
NEXT_PUBLIC_SUPABASE_URL: https://${DOMAIN_API}
|
|
|
|
|
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${ANON_KEY}
|
|
|
|
|
SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY}
|
|
|
|
|
NEXT_PUBLIC_SITE_URL: https://${DOMAIN_MAIN}
|
|
|
|
|
UFO_ROOT: /data/ufo
|
2026-05-24 01:45:00 +00:00
|
|
|
# W3.9 — /c/[slug] case-report viewer + BureauSnapshot read the
|
|
|
|
|
# markdown files the investigator-runtime writes. Mounted below.
|
|
|
|
|
CASE_ROOT: /data/ufo/case
|
W0+W1+W1.2: security hardening, observability, autocomplete, glitchtip, forgejo CI
W0 — security hardening (5 fixes verified live on disclosure.top)
- middleware: gate /api/admin/* same as /admin/* (F1)
- imgproxy: tighten LOCAL_FILESYSTEM_ROOT from / to /var/lib/storage (F2)
- studio: real basic-auth label (bcrypt hash, middleware reference) (F3)
- relations: ENABLE ROW LEVEL SECURITY + public SELECT policy (F4)
- migration 0003: fold is_searchable + hybrid_search update into canonical (TD#2)
W1 — observability + resilience + autocomplete
- studio: HOSTNAME=0.0.0.0 so Next.js binds on loopback for healthcheck
- compose: PG_POOL_MAX=20, CLAUDE_CODE_OAUTH_TOKEN gated by separate env
- claude-code.ts: subprocess timeout configurable (CLAUDE_CODE_TIMEOUT_MS)
- openrouter.ts: retry with exponential backoff + Retry-After + in-memory
circuit breaker (promotes FALLBACK after CB_THRESHOLD failures)
- lib/logger.ts: pino logger (NDJSON prod / pretty dev) + withRequest helper
- middleware: mints correlation_id, stamps x-correlation-id response header,
emits structured http_request log per /api/* call
- messages/route.ts: switch to structured logger
- 60_meili_index.py: push documents + chunks into Meilisearch
- /api/search/autocomplete: parallel meili search (docs + chunks), 5-8ms p50
- search-autocomplete.tsx: debounced dropdown wired into search-panel
W1.2 — Glitchtip + Forgejo self-hosted
- compose: glitchtip-redis + glitchtip-web + glitchtip-worker (v4.2)
- compose: forgejo + forgejo-runner (server v9, runner v6) with group_add=988
- @sentry/nextjs SDK wired (instrumentation.ts + sentry.{client,server}.config.ts)
- /api/admin/throw smoke endpoint (gated by W0-F1 middleware)
- Synthetic event ingestion verified at glitchtip.disclosure.top
- forgejo.disclosure.top up, repo discadmin/disclosure-bureau created,
runner registered (labels: ubuntu-latest, docker)
- .forgejo/workflows/ci.yml: typecheck + lint + build + npm audit + python
syntax + compose validation
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 21:18:42 +00:00
|
|
|
# W1-TD#10: bump pg pool from default 5 to 20 (chat agent + hybrid_search
|
|
|
|
|
# can saturate the smaller pool under concurrent load).
|
|
|
|
|
PG_POOL_MAX: ${PG_POOL_MAX:-20}
|
W3 followup: drop _FOR_WEB token, fix claude CLI args + writer guards, BIGSERIAL grants
Token consolidation:
- docker-compose web service now reads ${CLAUDE_CODE_OAUTH_TOKEN} directly,
drop the W1-F8 CLAUDE_CODE_OAUTH_TOKEN_FOR_WEB indirection (user feedback:
one var name, no _FOR_WEB suffix).
investigator-runtime claude.ts:
- --system-prompt silently dropped by CLI v2.1.150 for multi-KB prompts;
inline the system content into the user prompt with a separator
(mirrors scripts/reextract/run.py pattern).
- Multi-line prompts via positional -- broke ("Input must be provided …");
pipe via stdin instead.
- --allowedTools "" is rejected; when no tools wanted, omit it and explicitly
--disallowedTools the writer/reader set so the model can't reach for any.
investigator-runtime locard.ts:
- Log the raw response (first 600 chars) to container stderr — saved hours
of debugging when the writer rejected.
- Grade fallback: when Locard omits `grade` but provides custody_steps,
infer the highest grade that fits (≥3 → A, ≥2 → B, ≥1 → C).
investigator-runtime write_evidence.ts:
- Filter related_hypotheses entries with empty/null hypothesis_id silently
(Locard sometimes emits [{}] when it knows no link yet) instead of
failing the whole write.
Migration 0006_investigator_serial_sequences.sql:
- BIGSERIAL on the 7 investigation tables created auto-sequences
(evidence_evidence_pk_seq etc) that 0004 forgot to GRANT to the
investigator role. Without those grants every INSERT failed with
"permission denied for sequence …". Grant USAGE/SELECT/UPDATE on each
auto-seq.
Verified live: Locard wrote E-0002 + E-0003 from real Sandia chunks
(green fireball Feb 1949; cobalt particle analysis). Grade B, confidence
high, custody chain of 3 steps with honest gaps. Cost $0.09 for both,
~70s wall.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 00:05:35 +00:00
|
|
|
# Chat agent. Single source of truth — `CLAUDE_CODE_OAUTH_TOKEN` is the
|
|
|
|
|
# only OAuth var in the stack. The investigator-runtime reads the same
|
|
|
|
|
# one. When CHAT_PROVIDER=openrouter (default) the web container ignores
|
|
|
|
|
# this var at runtime, so no harm in exposing it.
|
|
|
|
|
CLAUDE_CODE_OAUTH_TOKEN: ${CLAUDE_CODE_OAUTH_TOKEN}
|
2026-05-18 01:44:36 +00:00
|
|
|
CLAUDE_CODE_MODEL: ${CLAUDE_CODE_MODEL}
|
|
|
|
|
OPENROUTER_API_KEY: ${OPENROUTER_API_KEY}
|
|
|
|
|
OPENROUTER_MODEL: ${OPENROUTER_MODEL}
|
|
|
|
|
OPENROUTER_FALLBACK_MODEL: ${OPENROUTER_FALLBACK_MODEL}
|
|
|
|
|
CHAT_PROVIDER: ${CHAT_PROVIDER}
|
|
|
|
|
# Meilisearch (used by /api/search)
|
|
|
|
|
MEILISEARCH_URL: http://meilisearch:7700
|
|
|
|
|
MEILISEARCH_API_KEY: ${MEILI_MASTER_KEY}
|
|
|
|
|
# Embed service (used by /lib/retrieval)
|
|
|
|
|
EMBED_SERVICE_URL: http://embed:8000
|
|
|
|
|
# pgvector + chunks (hybrid_search)
|
|
|
|
|
DATABASE_URL: postgres://postgres:${POSTGRES_PASSWORD}@db:5432/postgres
|
W0+W1+W1.2: security hardening, observability, autocomplete, glitchtip, forgejo CI
W0 — security hardening (5 fixes verified live on disclosure.top)
- middleware: gate /api/admin/* same as /admin/* (F1)
- imgproxy: tighten LOCAL_FILESYSTEM_ROOT from / to /var/lib/storage (F2)
- studio: real basic-auth label (bcrypt hash, middleware reference) (F3)
- relations: ENABLE ROW LEVEL SECURITY + public SELECT policy (F4)
- migration 0003: fold is_searchable + hybrid_search update into canonical (TD#2)
W1 — observability + resilience + autocomplete
- studio: HOSTNAME=0.0.0.0 so Next.js binds on loopback for healthcheck
- compose: PG_POOL_MAX=20, CLAUDE_CODE_OAUTH_TOKEN gated by separate env
- claude-code.ts: subprocess timeout configurable (CLAUDE_CODE_TIMEOUT_MS)
- openrouter.ts: retry with exponential backoff + Retry-After + in-memory
circuit breaker (promotes FALLBACK after CB_THRESHOLD failures)
- lib/logger.ts: pino logger (NDJSON prod / pretty dev) + withRequest helper
- middleware: mints correlation_id, stamps x-correlation-id response header,
emits structured http_request log per /api/* call
- messages/route.ts: switch to structured logger
- 60_meili_index.py: push documents + chunks into Meilisearch
- /api/search/autocomplete: parallel meili search (docs + chunks), 5-8ms p50
- search-autocomplete.tsx: debounced dropdown wired into search-panel
W1.2 — Glitchtip + Forgejo self-hosted
- compose: glitchtip-redis + glitchtip-web + glitchtip-worker (v4.2)
- compose: forgejo + forgejo-runner (server v9, runner v6) with group_add=988
- @sentry/nextjs SDK wired (instrumentation.ts + sentry.{client,server}.config.ts)
- /api/admin/throw smoke endpoint (gated by W0-F1 middleware)
- Synthetic event ingestion verified at glitchtip.disclosure.top
- forgejo.disclosure.top up, repo discadmin/disclosure-bureau created,
runner registered (labels: ubuntu-latest, docker)
- .forgejo/workflows/ci.yml: typecheck + lint + build + npm audit + python
syntax + compose validation
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 21:18:42 +00:00
|
|
|
# W1.2 — Glitchtip error monitoring (DSN issued by manage.py bootstrap)
|
|
|
|
|
SENTRY_DSN: ${GLITCHTIP_WEB_DSN}
|
|
|
|
|
NEXT_PUBLIC_SENTRY_DSN: ${GLITCHTIP_WEB_DSN}
|
2026-05-18 01:44:36 +00:00
|
|
|
volumes:
|
|
|
|
|
- ${DATA_WIKI}:/data/ufo/wiki:ro
|
|
|
|
|
- ${DATA_PROCESSING}:/data/ufo/processing:ro
|
|
|
|
|
- ${DATA_RAW}:/data/ufo/raw:ro
|
2026-05-24 01:45:00 +00:00
|
|
|
# W3.9 — case/ is the investigator-runtime's write surface; web reads it.
|
|
|
|
|
- ${CASE_ROOT:-/data/disclosure/case}:/data/ufo/case:ro
|
2026-05-18 01:44:36 +00:00
|
|
|
labels:
|
|
|
|
|
- traefik.enable=true
|
|
|
|
|
- traefik.docker.network=traefik-public
|
|
|
|
|
- traefik.http.routers.disclosure-web.rule=Host(`app.${DOMAIN_MAIN}`) || Host(`${DOMAIN_MAIN}`) || Host(`www.${DOMAIN_MAIN}`)
|
|
|
|
|
- traefik.http.routers.disclosure-web.entrypoints=websecure
|
|
|
|
|
- traefik.http.routers.disclosure-web.tls=true
|
|
|
|
|
- traefik.http.routers.disclosure-web.tls.certresolver=letsencrypt
|
|
|
|
|
- traefik.http.services.disclosure-web.loadbalancer.server.port=3000
|
|
|
|
|
# www → apex redirect
|
|
|
|
|
- traefik.http.middlewares.disclosure-www-redir.redirectregex.regex=^https?://www\.${DOMAIN_MAIN}/(.*)
|
|
|
|
|
- traefik.http.middlewares.disclosure-www-redir.redirectregex.replacement=https://${DOMAIN_MAIN}/$${1}
|
|
|
|
|
- traefik.http.middlewares.disclosure-www-redir.redirectregex.permanent=true
|
|
|
|
|
|
W3.1-W3.4: Investigation Bureau foundation — migrations, runtime, Locard
Migrations:
- 0004_investigation_bureau.sql: 7 new tables (investigation_jobs + evidence,
hypotheses, contradictions, witnesses, gaps, residual_uncertainties), id
sequences, pg_notify trigger on investigation_jobs, RLS read-only public,
investigator role with least-privilege grants (no service_role).
- 0005_investigator_write_policies.sql: fixup adding RLS INSERT/UPDATE
policies bound to investigator + service_role + postgres (RLS with only a
SELECT policy was silently blocking the worker's claim UPDATE).
investigator-runtime/ (new Bun + TS container):
- src/main.ts: LISTEN/NOTIFY poller, claim-with-SKIP-LOCKED, drain pool,
healthcheck file, graceful SIGTERM shutdown.
- src/orchestrator.ts: chief-detective dispatch (evidence_chain → Locard).
Marks job failed when all per-item outputs error; surfaces first errors.
- src/lib/{env,pg,audit,ids,claude}.ts: typed config (gate #8), pool +
dedicated LISTEN client, NDJSON audit, sequence allocator (E-NNNN etc),
claude -p subprocess with quota detection (api_error_status=429).
- src/tools/write_evidence.ts: schema-validate (grade A/B/C custody steps),
resolve chunk_pk via FK, verify verbatim_excerpt actually appears in
chunk content, INSERT + render case/evidence/E-NNNN.md + audit.
- src/detectives/locard.ts: load chunk → call Claude with locard.md system
prompt → parse strict JSON → call writeEvidence locally.
- Dockerfile installs `claude` CLI (OAuth) at build time.
Compose:
- new `investigator` service builds from investigator-runtime/, connects
with low-privilege role, mounts case/ RW and wiki/+raw/ RO, 512m mem cap.
Web:
- /api/admin/investigate/test (POST+GET) gated by middleware (W0-F1).
POST creates a job, GET polls status. For W3.6 it becomes the chat tool.
End-to-end smoke: INSERT job → pg_notify → claim → Locard dispatch →
claude subprocess invoked. Auth works (CLI v2.1.150). Currently quota
exhausted (weekly limit · resets 3pm UTC) — pipeline catches the typed
isQuota error, marks job failed with surfaced reason. Architecture proven;
quota reset enables real evidence creation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 22:49:33 +00:00
|
|
|
# ─── Investigation Bureau runtime — W3.1+ ─────────────────────────────────
|
|
|
|
|
#
|
|
|
|
|
# 8 detectives + chief-detective orchestrator. Listens on Postgres
|
|
|
|
|
# LISTEN/NOTIFY (channel `investigation_jobs`), spawns `claude -p`
|
|
|
|
|
# subprocesses (Sonnet via OAuth Max 20x) to produce evidence, hypotheses,
|
|
|
|
|
# contradictions, etc. Writes go through gated tools that validate schema
|
|
|
|
|
# + chunk references before INSERT.
|
|
|
|
|
#
|
|
|
|
|
# Connects with the LOW-PRIVILEGE `investigator` role (not service_role).
|
|
|
|
|
# Mounts case/ as RW and wiki/ as RO. Reads its OAuth token from env.
|
|
|
|
|
investigator:
|
|
|
|
|
container_name: disclosure-investigator
|
|
|
|
|
build:
|
|
|
|
|
context: /data/disclosure/investigator-runtime # synced from laptop, mirrors web/ pattern
|
|
|
|
|
dockerfile: Dockerfile
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
networks: [internal]
|
|
|
|
|
depends_on:
|
|
|
|
|
db: { condition: service_healthy }
|
|
|
|
|
embed: { condition: service_healthy }
|
|
|
|
|
environment:
|
|
|
|
|
DATABASE_URL: postgres://investigator:${INVESTIGATOR_DB_PASSWORD}@db:5432/postgres
|
|
|
|
|
EMBED_SERVICE_URL: http://embed:8000
|
|
|
|
|
CLAUDE_CODE_OAUTH_TOKEN: ${CLAUDE_CODE_OAUTH_TOKEN}
|
|
|
|
|
CLAUDE_MODEL: ${INVESTIGATOR_MODEL:-sonnet}
|
|
|
|
|
MAX_PARALLEL_WORKERS: ${INVESTIGATOR_MAX_PARALLEL_WORKERS:-2}
|
|
|
|
|
BUDGET_CAP_USD_PER_JOB: ${INVESTIGATOR_BUDGET_CAP_USD_PER_JOB:-1.00}
|
|
|
|
|
JOB_TIMEOUT_SECONDS: ${INVESTIGATOR_JOB_TIMEOUT_SECONDS:-300}
|
|
|
|
|
CASE_ROOT: /data/ufo/case
|
|
|
|
|
WIKI_ROOT: /data/ufo/wiki
|
|
|
|
|
AUDIT_LOG: /data/ufo/case/audit.jsonl
|
|
|
|
|
volumes:
|
|
|
|
|
- ${DATA_RAW}:/data/ufo/raw:ro
|
|
|
|
|
- ${DATA_WIKI}:/data/ufo/wiki:ro
|
|
|
|
|
- ${CASE_ROOT:-/data/disclosure/case}:/data/ufo/case
|
|
|
|
|
deploy:
|
|
|
|
|
resources:
|
|
|
|
|
limits:
|
|
|
|
|
memory: 512m
|
|
|
|
|
|
2026-05-18 01:44:36 +00:00
|
|
|
# ─── BGE-M3 embedding + reranker service (CPU only) ───────────────────────
|
|
|
|
|
embed:
|
|
|
|
|
container_name: disclosure-embed
|
|
|
|
|
build:
|
|
|
|
|
context: ../embed-service
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
networks: [internal]
|
|
|
|
|
environment:
|
|
|
|
|
DEVICE: cpu
|
|
|
|
|
EMBED_MODEL: BAAI/bge-m3
|
|
|
|
|
RERANK_MODEL: BAAI/bge-reranker-v2-m3
|
|
|
|
|
HF_HUB_DOWNLOAD_TIMEOUT: 600
|
|
|
|
|
volumes:
|
|
|
|
|
- hf-cache:/cache
|
|
|
|
|
healthcheck:
|
|
|
|
|
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8000/health"]
|
|
|
|
|
interval: 30s
|
|
|
|
|
timeout: 10s
|
|
|
|
|
retries: 5
|
|
|
|
|
start_period: 180s
|
|
|
|
|
deploy:
|
|
|
|
|
resources:
|
|
|
|
|
limits:
|
|
|
|
|
memory: 3g
|
W0+W1+W1.2: security hardening, observability, autocomplete, glitchtip, forgejo CI
W0 — security hardening (5 fixes verified live on disclosure.top)
- middleware: gate /api/admin/* same as /admin/* (F1)
- imgproxy: tighten LOCAL_FILESYSTEM_ROOT from / to /var/lib/storage (F2)
- studio: real basic-auth label (bcrypt hash, middleware reference) (F3)
- relations: ENABLE ROW LEVEL SECURITY + public SELECT policy (F4)
- migration 0003: fold is_searchable + hybrid_search update into canonical (TD#2)
W1 — observability + resilience + autocomplete
- studio: HOSTNAME=0.0.0.0 so Next.js binds on loopback for healthcheck
- compose: PG_POOL_MAX=20, CLAUDE_CODE_OAUTH_TOKEN gated by separate env
- claude-code.ts: subprocess timeout configurable (CLAUDE_CODE_TIMEOUT_MS)
- openrouter.ts: retry with exponential backoff + Retry-After + in-memory
circuit breaker (promotes FALLBACK after CB_THRESHOLD failures)
- lib/logger.ts: pino logger (NDJSON prod / pretty dev) + withRequest helper
- middleware: mints correlation_id, stamps x-correlation-id response header,
emits structured http_request log per /api/* call
- messages/route.ts: switch to structured logger
- 60_meili_index.py: push documents + chunks into Meilisearch
- /api/search/autocomplete: parallel meili search (docs + chunks), 5-8ms p50
- search-autocomplete.tsx: debounced dropdown wired into search-panel
W1.2 — Glitchtip + Forgejo self-hosted
- compose: glitchtip-redis + glitchtip-web + glitchtip-worker (v4.2)
- compose: forgejo + forgejo-runner (server v9, runner v6) with group_add=988
- @sentry/nextjs SDK wired (instrumentation.ts + sentry.{client,server}.config.ts)
- /api/admin/throw smoke endpoint (gated by W0-F1 middleware)
- Synthetic event ingestion verified at glitchtip.disclosure.top
- forgejo.disclosure.top up, repo discadmin/disclosure-bureau created,
runner registered (labels: ubuntu-latest, docker)
- .forgejo/workflows/ci.yml: typecheck + lint + build + npm audit + python
syntax + compose validation
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 21:18:42 +00:00
|
|
|
|
|
|
|
|
# ─── Glitchtip — self-hosted Sentry-compatible error monitor (W1.2) ───────
|
|
|
|
|
glitchtip-redis:
|
|
|
|
|
container_name: disclosure-glitchtip-redis
|
|
|
|
|
image: redis:7-alpine
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
networks: [internal]
|
|
|
|
|
volumes:
|
|
|
|
|
- glitchtip-redis-data:/data
|
|
|
|
|
command: redis-server --appendonly yes
|
|
|
|
|
|
|
|
|
|
glitchtip-web:
|
|
|
|
|
container_name: disclosure-glitchtip-web
|
|
|
|
|
image: glitchtip/glitchtip:v4.2
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
networks: [internal, traefik]
|
|
|
|
|
depends_on:
|
|
|
|
|
db: { condition: service_healthy }
|
|
|
|
|
glitchtip-redis: { condition: service_started }
|
|
|
|
|
environment:
|
|
|
|
|
DATABASE_URL: postgres://glitchtip:${GLITCHTIP_DB_PASSWORD}@db:5432/glitchtip
|
|
|
|
|
SECRET_KEY: ${GLITCHTIP_SECRET_KEY}
|
|
|
|
|
REDIS_URL: redis://glitchtip-redis:6379/0
|
|
|
|
|
PORT: "8080"
|
|
|
|
|
GLITCHTIP_DOMAIN: ${GLITCHTIP_DOMAIN}
|
|
|
|
|
DEFAULT_FROM_EMAIL: ${GLITCHTIP_DEFAULT_FROM_EMAIL}
|
|
|
|
|
EMAIL_URL: consolemail://
|
|
|
|
|
ENABLE_USER_REGISTRATION: "false" # bootstrap admin via manage.py
|
|
|
|
|
ENABLE_ORGANIZATION_CREATION: "false"
|
|
|
|
|
CELERY_WORKER_AUTOSCALE: "1,3"
|
|
|
|
|
CELERY_WORKER_MAX_TASKS_PER_CHILD: "10000"
|
|
|
|
|
volumes:
|
|
|
|
|
- glitchtip-uploads:/code/uploads
|
|
|
|
|
labels:
|
|
|
|
|
- traefik.enable=true
|
|
|
|
|
- traefik.docker.network=traefik-public
|
|
|
|
|
- traefik.http.routers.disclosure-glitchtip.rule=Host(`glitchtip.disclosure.top`)
|
|
|
|
|
- traefik.http.routers.disclosure-glitchtip.entrypoints=websecure
|
|
|
|
|
- traefik.http.routers.disclosure-glitchtip.tls=true
|
|
|
|
|
- traefik.http.routers.disclosure-glitchtip.tls.certresolver=letsencrypt
|
|
|
|
|
- traefik.http.services.disclosure-glitchtip.loadbalancer.server.port=8080
|
|
|
|
|
|
|
|
|
|
glitchtip-worker:
|
|
|
|
|
container_name: disclosure-glitchtip-worker
|
|
|
|
|
image: glitchtip/glitchtip:v4.2
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
networks: [internal]
|
|
|
|
|
depends_on:
|
|
|
|
|
db: { condition: service_healthy }
|
|
|
|
|
glitchtip-redis: { condition: service_started }
|
|
|
|
|
environment:
|
|
|
|
|
DATABASE_URL: postgres://glitchtip:${GLITCHTIP_DB_PASSWORD}@db:5432/glitchtip
|
|
|
|
|
SECRET_KEY: ${GLITCHTIP_SECRET_KEY}
|
|
|
|
|
REDIS_URL: redis://glitchtip-redis:6379/0
|
|
|
|
|
GLITCHTIP_DOMAIN: ${GLITCHTIP_DOMAIN}
|
|
|
|
|
DEFAULT_FROM_EMAIL: ${GLITCHTIP_DEFAULT_FROM_EMAIL}
|
|
|
|
|
EMAIL_URL: consolemail://
|
|
|
|
|
CELERY_WORKER_AUTOSCALE: "1,3"
|
|
|
|
|
CELERY_WORKER_MAX_TASKS_PER_CHILD: "10000"
|
|
|
|
|
volumes:
|
|
|
|
|
- glitchtip-uploads:/code/uploads
|
|
|
|
|
command: ./bin/run-celery-with-beat.sh
|
|
|
|
|
|
|
|
|
|
# ─── Forgejo — self-hosted Git + Actions CI (W1.2) ────────────────────────
|
|
|
|
|
forgejo:
|
|
|
|
|
container_name: disclosure-forgejo
|
|
|
|
|
image: codeberg.org/forgejo/forgejo:9
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
networks: [internal, traefik]
|
|
|
|
|
depends_on:
|
|
|
|
|
db: { condition: service_healthy }
|
|
|
|
|
environment:
|
|
|
|
|
USER_UID: "1000"
|
|
|
|
|
USER_GID: "1000"
|
|
|
|
|
FORGEJO__database__DB_TYPE: postgres
|
|
|
|
|
FORGEJO__database__HOST: db:5432
|
|
|
|
|
FORGEJO__database__NAME: forgejo
|
|
|
|
|
FORGEJO__database__USER: forgejo
|
|
|
|
|
FORGEJO__database__PASSWD: ${FORGEJO_DB_PASSWORD}
|
|
|
|
|
FORGEJO__server__DOMAIN: ${FORGEJO_DOMAIN}
|
|
|
|
|
FORGEJO__server__ROOT_URL: https://${FORGEJO_DOMAIN}
|
|
|
|
|
FORGEJO__server__SSH_DOMAIN: ${FORGEJO_DOMAIN}
|
|
|
|
|
FORGEJO__service__DISABLE_REGISTRATION: "true" # admin invites only
|
|
|
|
|
FORGEJO__actions__ENABLED: "true"
|
|
|
|
|
FORGEJO__security__INSTALL_LOCK: "true"
|
|
|
|
|
volumes:
|
|
|
|
|
- forgejo-data:/data
|
|
|
|
|
labels:
|
|
|
|
|
- traefik.enable=true
|
|
|
|
|
- traefik.docker.network=traefik-public
|
|
|
|
|
- traefik.http.routers.disclosure-forgejo.rule=Host(`forgejo.disclosure.top`)
|
|
|
|
|
- traefik.http.routers.disclosure-forgejo.entrypoints=websecure
|
|
|
|
|
- traefik.http.routers.disclosure-forgejo.tls=true
|
|
|
|
|
- traefik.http.routers.disclosure-forgejo.tls.certresolver=letsencrypt
|
|
|
|
|
- traefik.http.services.disclosure-forgejo.loadbalancer.server.port=3000
|
|
|
|
|
|
|
|
|
|
forgejo-runner:
|
|
|
|
|
container_name: disclosure-forgejo-runner
|
|
|
|
|
image: code.forgejo.org/forgejo/runner:6
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
networks: [internal]
|
|
|
|
|
# GID of the docker group on the host — lets the runner (uid 1000) talk
|
|
|
|
|
# to the docker socket without running as root.
|
|
|
|
|
group_add:
|
|
|
|
|
- "988"
|
|
|
|
|
depends_on:
|
|
|
|
|
forgejo: { condition: service_started }
|
|
|
|
|
environment:
|
|
|
|
|
FORGEJO_INSTANCE_URL: http://forgejo:3000
|
|
|
|
|
FORGEJO_RUNNER_REGISTRATION_TOKEN: ${FORGEJO_RUNNER_TOKEN}
|
|
|
|
|
FORGEJO_RUNNER_NAME: disclosure-runner
|
|
|
|
|
volumes:
|
|
|
|
|
- forgejo-runner-config:/data
|
|
|
|
|
- /var/run/docker.sock:/var/run/docker.sock
|
|
|
|
|
command:
|
|
|
|
|
- sh
|
|
|
|
|
- -c
|
|
|
|
|
- |
|
|
|
|
|
sleep 10
|
|
|
|
|
if [ ! -f /data/.runner ]; then
|
|
|
|
|
forgejo-runner register --no-interactive --instance "$$FORGEJO_INSTANCE_URL" --token "$$FORGEJO_RUNNER_REGISTRATION_TOKEN" --name "$$FORGEJO_RUNNER_NAME" --labels 'ubuntu-latest:docker://node:20-bookworm,docker:host'
|
|
|
|
|
fi
|
|
|
|
|
forgejo-runner daemon
|