DESIGN.md: Exchange8:Surveillance — Detailed Design Document
1. Scope & Context
Owns
- Detection alerts (
surveillance.surveillance_alert) — one row per detection: pattern (CHECK-constrained to the eight pattern keys), instrument, account, confidence (0–100), evidence JSONB. - Case management (
surveillance.surveillance_case) — the compliance workflow per alert:open / investigating / closed, dismissal flag, assignee, open/close timestamps, FK to the alert. - Operational alerts (
surveillance.alert) — the unacknowledged ops-alert queue (admin_id IS NULL= unacked; partial index for the ack sweep). - The pure detection core: the eight
Patterns with severity ranking, theObservationcontract, the wash-trade / spoofing / marking-the-close detectors, and theCaseManagerworkflow. - Audit journaling and the outbox producer for all of the above.
Does NOT own (delegates)
- Regulatory reports — Exchange8:Regulatory consumes closed cases; this service only produces the detection + workflow record.
- Where the trade/order observations come from — Exchange8:ME / Exchange8:Clearing / Exchange8:Gateway / Exchange8:OMS produce the event stream; Sentin8:Bus delivers it.
- Tenant-authored detection modules (layering, momentum ignition, insider,
front-running, excessive crossing when tenant-authored) — Sentin8:Func
executes them; this service defines only the
Pattern/Alertcontract. - Identity, tokens, M2M client registration — Sentin8:Auth.
- Trigger scheduling — Sentin8:Cron (none declared by this service today).
System context
Container view
2. Runtime Architecture
The binary (src/main.rs) is a Sentin8 Shell assembly:
Sentin8Shell::new("exchange8:surveillance", "EXCHANGE8_SURVEILLANCE") with
registration printing, with_simple_state(), and with_router(extend_router)
that mounts the policy-admin API. The global allocator is mimalloc
(#[global_allocator]), covering all in-process tokio workers. Boot, DB
pool, drain, relay and audit pusher are shell-provided. The detectors run on
the Bus-consumer task — the HTTP router carries health + admin endpoints
only.
Request family: detection (observation → alert → case)
Request family: case workflow (compliance review)
Request family: operational alert ack
3. Data Model
Schema-per-service: database exchange8, schema surveillance, role
exchange8_surveillance_service, DSN env EXCHANGE8_SURVEILLANCE_DATABASE_URL.
RLS (ENABLE + FORCE) on every table; policies require
sentin8_context_initialized() and env_id = sentin8_current_env_id()
(set via SELECT sentin8_set_tenant_context(...) on the same connection).
No cross-schema grants; cross_schema_grants: [] in registration.
Per-table notes:
| Table | Visibility | Notes |
|---|---|---|
surveillance.surveillance_alert | env-scoped, RLS FORCE | pattern ∈ {wash_trade, spoofing, layering, momentum_ignition, insider_trading, front_running, marking_the_close, excessive_crossing}; confidence CHECK 0–100; index (env_id, pattern) |
surveillance.surveillance_case | env-scoped, RLS FORCE | FK to surveillance_alert(id); state CHECK open/investigating/closed; index (env_id, state) for the queue view |
surveillance.alert | env-scoped, RLS FORCE | ops alert log; unacked = admin_id IS NULL; partial index (env_id) WHERE admin_id IS NULL |
surveillance.outbox | env-scoped, RLS FORCE | DBX §6.7 producer table; partial index on relayed = false; pg_notify('surveillance_outbox_changed') trigger |
surveillance.audit_journal | env-scoped, RLS FORCE | DBX §6.8 journal; partial index on pushed = false |
Lifecycles
4. API Surface
Grouped by audience; the authorization gate for all handler surfaces is the
8-step platform pipeline — bearer verify → tenant resolve → feature gate →
Shield (exchange8:surveillance-policy scope-mirror, enforced) → RLS
context → execute → audit. The HTTP router mounts health, /openapi.json and
the Shield policy-admin API only; domain work arrives via bus events and
(Sentin8:Func-triggered) detections. The single action the step-5 gate
evaluates today is exchange8:surveillance:alert (tests/policy.rs
HANDLER_ACTIONS).
| Audience | Surface | Gate | Emits |
|---|---|---|---|
| System (M2M) | Detection consume (exchange8:surveillance:alert action) | scope-mirror permit / sentin8:sys admin scopes | outbox event (see §10) |
| System (M2M) | Bus produce/consume workloads exchange8:surveillance (+ surveillance short spelling) | sentin8:bus:produce / sentin8:bus:consume scopes | — |
| System (M2M) | Policy hot-reload + stat workloads exchange8:surveillance-policy / surveillance-policy | sentin8:bus:consume / sentin8:stat:measure scopes | — |
| System (M2M) | Audit journal pusher workload surveillance-audit | sentin8:sys:audit:write scope | — |
| System (M2M) | TLS host-context resolver workload exchange8-surveillance-tls | sentin8:sys:instances:read scope (GET /v1/resolve/hostname) | — |
| Admin (RFW) | /v1/admin/ui/alerts + /v1/admin/alerts (route alerts, label Alerts, icon 0xe8d2) | exchange8:surveillance:read / role exchange8:admin | — |
| Admin (RFW) | /v1/admin/ui/cases + /v1/admin/cases (route cases, label Cases, icon 0xe8a0) | exchange8:surveillance:read / :write / role exchange8:admin | — |
| Admin | Shield policy-admin API (exchange8:surveillance:policy:read / :write) | role exchange8:surveillance:admin | sentin8:sys:policy family |
| Ops | GET /v1/health, /openapi.json | unauthenticated (health), none | — |
Declared scopes: exchange8:surveillance:read / exchange8:surveillance:write
/ exchange8:alert:read / exchange8:alert:ack (admin-role grants under
exchange8:admin). Declared error codes: exchange8_case_not_found (404).
Detection-to-case chain (outbox → Bus → trigger):
5. Cross-Cutting Concerns
- Authentication — Sentin8:Auth is the only issuer. Detection work is
system-driven: cert-bound M2M
client_credentialstokens (DCR-registered per workload:exchange8:surveillance,exchange8:surveillance-policy,surveillance-policy,surveillance-audit,exchange8-surveillance-tls). DCR is fail-closed: only workloads present insentin8_service_workloads(seeded bysentin8-cli vertical enroll) can register. - Authorization — the scope-mirror policy set (
origin='platform',mode='enforced') permits the declared action (exchange8:surveillance:alert) for callers holding the matching scope or the platform-admin scopes (sentin8:sys:admin,sentin8:sys:org:admin,sentin8:sys:env:admin). No-set → deny (fail-closed); shadow is an emergency rollback lever only;token_tpnever bypasses policy (tests/policy.rs proves all of these against the real seeded text). - Tenant resolution + RLS — every table has
ENABLE+FORCE ROW LEVEL SECURITYwith policiessentin8_context_initialized() AND env_id = sentin8_current_env_id(); the context is set withSELECT sentin8_set_tenant_context(...)(set_config(..., true), transaction-local) on the same connection as the data statements — the async helper must be awaited or RLS silently sees no context. Explicitenv_id = $Npredicates remain the primary defense; RLS is the backstop. Roleexchange8_surveillance_servicecarries noBYPASSRLS. - Shield as the engine's only name — the embedded declarative policy
engine (
sentin8-policy) is branded Shield in docs, comments and UI; the vendor name appears only in the dependency mapping. - Idempotency — case opening dedupes per (pattern, instrument, account,
day) in the shell consumer; bus produce rides
idempotency_key= outbox row id; detector buffers fire-once-per-signature (spoofing clears its buffer on fire) so replays do not duplicate alerts. - Rate limiting / bot protection — ingress tier is the platform edge (Sentin8:Proxy); service-local rate policies ride the Shield policy surface (per-tenant sets), not bespoke code.
- Audit journaling — every state transition writes
surveillance.audit_journal(same transaction where possible); the pusher drains toPOST /v1/audit; Sys outage retries without loss. - Drain —
sentin8::shellgraceful drain on upgrade: HTTP stops accepting, in-flight requests finish, the relay/journal pusher checkpoint unrelayed/unpushed rows, and Sentin8:Captain'supgrade-instanceswaps the binary (rolling, zero-downtime). - TLS — terminated in-process by
sentin8-tlsbehind Sentin8:Proxy (single ingress, SNIexchange8-surveillance.sys.ops…); theexchange8-surveillance-tlsworkload resolves host context DB-free viaGET /v1/resolve/hostname(sentin8:sys:instances:read). Post-quantum only; no classical fallback. - Allocator — mimalloc global allocator covers HTTP, bus-consumer, relay, journal pusher and future purge workers alike.