feat: align LegacyHUB with TeamHUB platform contract (D2/D4, assets, security)

Close 12 audit-driven platform-compliance gaps on a single branch.

- D4 dispatch: app/integrations/dispatch_client.py participant `legacyhub`,
  emits LegacyhubDocumentIndexed + AssetDerivativeReady after the indexing
  commit (idempotent uuid5), http_inbox route (reindex/tombstone) with
  audit-based dedupe; docs/dispatch-contract.md. Celery+Redis stays intra-module.
- D2 SSO: app/integrations/identity.py validates X-TeamHub-* + role/scope
  mapper; security.py adds trusted-header enforcement (AUTH_REQUIRE_IDENTITY)
  and a scope check on /search; docker-compose.teamhub.yml (external teamhub_net
  + internal db net, api not host-published); RUNBOOK network/firewall section.
- Asset standard: SearchHit/Citation carry asset_id/owner_module; buckets
  renamed teamhub-legacyhub-* (+quarantine/tmp/exports); purge-by-asset_id with
  legal-hold guard (app/indexing/projection.py); OCR-markdown derivative event.
- audit_log model + Alembic 0003 + record_audit on writes (same transaction).
- Secret masking: app/common/json_logger.py recursive mask wired into structlog
  (+ensure_ascii=False); event payloads redacted before persistence.
- Service X-API-Key mandatory on ingest endpoints (defence-in-depth).
- Port: host API 8000->8050 (collision with SalesHUB/MailHUB resolved),
  container still listens on 8000.
- Config: no plaintext secret defaults; fail-loud in non-dev (no value leak).
- Docs drift: README PG 5440, layered-auth note, 5173 removed from CORS;
  ingest/folder gated by ENABLE_FOLDER_INGEST (410 by default).
- ADRs: layers mapping, shared-core extraction, UI locale (RU-first).

Tests: 78 passing (ruff, compileall, pytest, tsc, vite build, compose config).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vadim Malanov
2026-06-15 11:44:15 +03:00
parent 9af557c26a
commit d27dd0ffbb
45 changed files with 2149 additions and 61 deletions

60
docs/dispatch-contract.md Normal file
View File

@@ -0,0 +1,60 @@
# LegacyHUB — dispatch participant contract
Status: draft · 2026-06-15 · D4 / 08_DECISIONS, INTER_MODULE_CONTRACT §2
LegacyHUB joins the shared platform RabbitMQ via `dispatch-api` (not directly).
Internal background work stays on Celery+Redis; this contract covers only the
inter-module bus.
## Participant
| Field | Value |
|---|---|
| `participant_code` | `legacyhub` |
| `participant_type` | `service` |
| Auth to dispatch-api | `X-API-Key` (env `DISPATCH_API_KEY`) |
| Delivery mode (inbound) | `http_inbox``POST /api/v1/dispatch/inbox` + business-confirm |
| Inbox auth | service `X-API-Key` (`require_service_api_key`) |
Configuration: `DISPATCH_ENABLED`, `DISPATCH_API_URL`, `DISPATCH_API_KEY`,
`DISPATCH_PARTICIPANT_CODE`, `DISPATCH_TIMEOUT_SECONDS`.
## Envelope (INTER_MODULE_CONTRACT §2.4)
```json
{
"message_id": "<uuid4, unique per send>",
"event_id": "<uuid5 over the business key, idempotent>",
"card_uid": "<asset_id or document_id>",
"message_type": "<Module><Event>",
"participant_code": "legacyhub",
"created_at": "<ISO-8601 UTC>",
"body": { "...": "JSON only — no binary, no presigned URLs, no local paths" }
}
```
## Published events
| `message_type` | When | Body (JSON) |
|---|---|---|
| `LegacyhubDocumentIndexed` | After a document reaches `INDEXING_COMPLETED` (post-commit) | `document_id`, and for asset ingests `asset_id`, `owner_module`, `owner_record_type`, `owner_record_id`, `manifest_version` |
| `AssetDerivativeReady` | After OCR/markdown derivative is produced for an asset ingest (respects `ingest_options.emit_events`) | `event_type=AssetDerivativeReady`, `asset_id`, `owner_*`, `derivative_type=ocr_markdown`, `content_type=text/markdown`, `source_asset_sha256`, `generator{pipeline,version}` |
Idempotency: `event_id = uuid5(namespace, business-key)`, so re-emits of the same
indexing/derivative are de-dupable by consumers.
## Subscriptions (inbound)
| `message_type` | Action |
|---|---|
| `ReindexRequested` / `LegacyhubReindexRequested` | Resolve `document_id` (directly or via `asset_id`) and enqueue the Celery reindex task |
Inbound delivery is at-least-once; the inbox is idempotent by `event_id`
(recorded in `audit_log` as `dispatch.inbox.processed`).
## Boundaries
- No direct RabbitMQ from this module — only `dispatch-api`.
- No binary payloads / presigned URLs / local filesystem paths in any body
(10 §7). Originals stay owner-module-owned; LegacyHUB references object storage
+ `asset_id` only.