Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,13 @@ lerna-debug.log*
.env
.env.local
.env.*.local

# Folded SSI Control Tower local artifacts
apps/ssi-control-tower/.venv/
apps/ssi-control-tower/.pytest_cache/
apps/ssi-control-tower/.ruff_cache/
apps/ssi-control-tower/**/__pycache__/
apps/ssi-control-tower/data/*.db
apps/ssi-control-tower/data/*.sqlite
apps/ssi-control-tower/exports/
apps/ssi-control-tower/cache/
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ coverage
playwright-report
test-results
pnpm-lock.yaml
apps/ssi-control-tower
7 changes: 7 additions & 0 deletions HANDOFF.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ Read this file first, then check live `git` / GitHub state before acting.

## Current repository state

- Repository: `https://github.com/Raafet57/payment-intelligence-modules`
- Local path on Hermes: `/Users/Shared/AgentWork/repos/payment-intelligence-modules`
- Active feature branch for SSI work: `feat/ssi-hybrid-instruction-flow`
- SSI Control Tower is being folded into `apps/ssi-control-tower/` as a separate FastAPI/Jinja backend module. The root Vite/React browser suite remains static/browser-only, and root `src/` plus `scripts/privacy-audit.sh src` must stay scoped to the browser runtime.

## Previous handoff snapshot

- Repository: `https://github.com/Raafet57/payment-intelligence-modules`
- Local path on Hermes: `/Users/Shared/AgentWork/repos/payment-intelligence-modules`
- Branch to start from next time: `main`
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ data.

For the latest repo/session handoff, start with [`HANDOFF.md`](./HANDOFF.md).

The root suite remains static and browser-only. Backend/product-control modules live under `apps/**` as separate, explicitly scoped applications and are not part of the root browser runtime.

## Modules

| Module | Route | Status | What it does |
Expand Down Expand Up @@ -69,6 +71,11 @@ banned APIs or absolute-URL `fetch` calls appear under `src/`.
- Node.js >= 20
- pnpm >= 9

For the folded SSI Control Tower backend module only:

- Python 3.11
- Run commands from `apps/ssi-control-tower/`; the module owns its own `pyproject.toml`, `Makefile`, tests, and SQLite development data.

## Scripts

```bash
Expand Down Expand Up @@ -154,6 +161,13 @@ scripts/
build-cbprplus-schema-manifest.ts
build-iban-registry.ts
privacy-audit.sh
apps/
ssi-control-tower/ # Separate FastAPI/Jinja backend module; not part of root browser runtime
app/ # Python app, routers, services, web templates
data/ # Synthetic fixtures/reference YAML only; local DBs ignored
tests/ # pytest suite for the SSI backend module
pyproject.toml # Python dependencies owned by the nested module
Makefile # module-local test/install commands
```

## Hosting notes (target-neutral)
Expand Down
13 changes: 13 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,19 @@ the suite is browser-only, static-host friendly, and privacy-first.

## Recommended next steps

### 0. SSI Control Tower folded backend module

SSI Control Tower now lives under `apps/ssi-control-tower/` as a separate FastAPI/Jinja backend module inside this repository. It is not part of the root static browser suite runtime, and it must not weaken the browser-only privacy boundary for `src/`.

Current branch scope is the approved hybrid unified SSI instruction flow, Phase -1 through Phase 2:

- fold the module into this repo as ordinary tracked files;
- keep CSV demo imports compatible while also creating canonical `SsiInstruction` records with source lineage;
- make SSI Plus V3 imports use the same canonical ingestion path;
- add instruction-level validation, instruction exceptions, and a unified control queue API.

Approvals, exports, readiness expansion, and full UI polish remain follow-up scope.

### 1. Deployment target decision

Pick one static hosting target and add only the minimal required SPA fallback
Expand Down
19 changes: 19 additions & 0 deletions apps/ssi-control-tower/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Python / local runtime
.venv/
__pycache__/
*.py[cod]
*.egg-info/
.pytest_cache/

# Generated runtime state
data/ssi.db
data/exports/
exports/

# Local agent metadata
.hermes/
.claude/

# OS/editor noise
.DS_Store
*.swp
16 changes: 16 additions & 0 deletions apps/ssi-control-tower/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
FROM python:3.11-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
SSI_DB_PATH=data/ssi.db \
SSI_EXPORT_DIR=data/exports

WORKDIR /app
COPY pyproject.toml README.md ./
COPY app ./app
COPY data ./data
COPY rules ./rules
COPY docs ./docs
RUN pip install --no-cache-dir --upgrade pip && pip install --no-cache-dir -e .[test]
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
21 changes: 21 additions & 0 deletions apps/ssi-control-tower/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
PYTHON ?= python3.11
VENV := .venv
PIP := $(VENV)/bin/pip
PY := $(VENV)/bin/python
UVICORN := $(VENV)/bin/uvicorn

$(PY): pyproject.toml
$(PYTHON) -m venv $(VENV)
$(PIP) install --upgrade pip
$(PIP) install -e ".[test]"

.PHONY: run test clean
run: $(PY)
$(UVICORN) app.main:app --host 0.0.0.0 --port 8000 --reload

test: $(PY)
$(PY) -m pytest -q

clean:
rm -rf $(VENV) .pytest_cache data/ssi.db data/exports __pycache__
find . -name "__pycache__" -type d -prune -exec rm -rf {} +
199 changes: 199 additions & 0 deletions apps/ssi-control-tower/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
# SSI Control Tower

SSI Control Tower is a T+1 Standing Settlement Instruction readiness and governance prototype. The thesis is simple: SSI automation is lifecycle governance, not field validation. A failed rule without owner, SLA, remediation, approval, and audit evidence is not a control, so the app combines ingestion, mapping, normalization, YAML rules, exception ownership, four-eyes approval, immutable audit, readiness scoring, dashboards, and approved-record export.

## Synthetic public prototype disclaimer

This project is a synthetic-data public prototype for SSI readiness and governance. It is not affiliated with DTCC, not affiliated with SSImple, not affiliated with Swift, not affiliated with FMSB, not affiliated with ISITC, and not affiliated with any custodian or market infrastructure. It does not connect to production SSI utilities and does not contain real settlement instructions, client data, account numbers, or proprietary workflows.

All demo data is synthetic. All account numbers are masked. No external APIs and no external LLMs are called.

## Quickstart

```bash
make run
```

Open:

- Web UI: <http://localhost:8000>
- API docs: <http://localhost:8000/docs>

The app creates `data/ssi.db`, loads `data/seed_users.yaml`, imports `data/seed_ssi.csv`, runs validation, creates exception cases, and renders the dashboard.

## Realistic synthetic sample files

Additional synthetic sample files are provided for richer SSI demonstrations:

- `data/sample_realistic_multi_market_ssi.csv` - app-compatible import CSV with 60 synthetic rows across 18 countries, 12 currencies, and 10 asset categories including Securities.
- `data/sample_swiftref_ssiplus_v3_synthetic.tsv` - SwiftRef `SSIPLUS_V3`-shaped tab-delimited synthetic sample with the 28-field structure from the SSI Plus technical specification.
- `docs/swiftref_ssi_structure_notes.md` - read-only structure notes explaining the SWIFTRef SSI Directory / SSI Plus concepts used to shape the samples.

These files are synthetic and contain no production SSI records or unmasked account numbers.

## V2 SSI Plus source-truth slice

V2 adds a first SSI Plus-shaped source-truth layer while preserving the V1 synthetic CSV flow. Uploading `data/sample_swiftref_ssiplus_v3_synthetic.tsv` to `/api/v1/imports` now auto-routes to the V2 importer, persists source lineage and domain `ssi_instructions`, and exposes control-first aggregates at `/api/v1/dashboard/v2/source-controls`. The home dashboard also shows a compact "SSI Plus source truth" section.

V2 privacy posture: source rejects, audit payloads, API responses, and dashboard aggregates avoid raw account values, source record keys, BDP keys, EIDs, institution names, and city values. See `docs/v2_ssiplus_source_truth_slice.md`.

## Tests

```bash
make test
```

The contract tests cover ingestion mapping, normalization, required-field rules, format rules, duplicate-active detection, stale checks, governance rules, exception workflows, four-eyes approvals, immutable audit triggers, readiness scoring, export filtering, and the full demo flow.

## Commands

```bash
make run # install local venv and start uvicorn
make test # install local venv and run pytest
make clean # remove venv, pytest cache, generated DB, and exports
```

## Docker Compose

```bash
docker compose up --build
```

## Architecture

```text
CSV upload / seed data
|
v
+---------------------+ +------------------+
| Mapping aliases | -----> | Normalization |
| data/field_aliases | | dates/codes/BICs |
+---------------------+ +------------------+
| |
v v
+--------------------------------------------------+
| SQLite canonical model: entities, accounts, SSIs |
+--------------------------------------------------+
|
v
+---------------------+ +-----------------------+
| YAML rule loader | ----> | Python evaluator set |
| rules/*.yaml | | app/rules/evaluators |
+---------------------+ +-----------------------+
|
v
+------------------+ +----------------+ +------------------+
| Validation | | Exceptions/SLA | | Four-eyes approval|
+------------------+ +----------------+ +------------------+
| | |
v v v
+--------------------------------------------------+
| Immutable audit events with SQLite no-update/no-delete triggers |
+--------------------------------------------------+
|
v
+------------------+ +------------------+
| Readiness score | | Approved exports |
+------------------+ +------------------+
```

## API demo flow

Run the app first with `make run`, then in a second terminal:

```bash
BASE=http://localhost:8000

curl -s $BASE/api/v1/dashboard/readiness-score | jq

curl -s "$BASE/api/v1/exceptions?severity=critical&rule_id=SSI.DUPLICATE.ACTIVE" | jq

PAIR_CONTEXT=$(curl -s "$BASE/api/v1/exceptions?severity=critical&rule_id=SSI.DUPLICATE.ACTIVE" | jq -r 'group_by(.context_key)[] | select(length==2) | .[0].context_key' | head -1)
STALE_EXCEPTION=$(curl -s "$BASE/api/v1/exceptions?severity=critical&rule_id=SSI.DUPLICATE.ACTIVE" | jq -r --arg ctx "$PAIR_CONTEXT" '.[] | select(.context_key==$ctx) | select(.ssi_id as $id | true) | .exception_id' | head -1)
STALE_SSI=$(curl -s "$BASE/api/v1/exceptions?severity=critical&rule_id=SSI.DUPLICATE.ACTIVE" | jq -r --arg ctx "$PAIR_CONTEXT" '.[] | select(.context_key==$ctx) | .ssi_id' | head -1)
REMAINING_SSI=$(curl -s "$BASE/api/v1/exceptions?severity=critical&rule_id=SSI.DUPLICATE.ACTIVE" | jq -r --arg ctx "$PAIR_CONTEXT" --arg retired "$STALE_SSI" '.[] | select(.context_key==$ctx and .ssi_id!=$retired) | .ssi_id' | head -1)

curl -s -X PATCH "$BASE/api/v1/exceptions/$STALE_EXCEPTION/assign" \
-H 'Content-Type: application/json' \
-H 'X-User-Email: analyst@example.com' \
-d '{"owner_user_email":"ops-owner@example.com"}' | jq

curl -s -X POST "$BASE/api/v1/ssis/$STALE_SSI/retire" \
-H 'X-User-Email: ops-owner@example.com' | jq

APPROVAL_ID=$(curl -s -X POST "$BASE/api/v1/ssis/$REMAINING_SSI/submit-for-approval" \
-H 'Content-Type: application/json' \
-H 'X-User-Email: ops-owner@example.com' \
-d '{"change_payload":{"local_agent_bic":"AGNTFRPPXXX"},"change_summary":"Correct local agent BIC after owner review","evidence_reference":"EV-001"}' | jq -r .approval_id)

curl -i -X POST "$BASE/api/v1/approvals/$APPROVAL_ID/approve" \
-H 'X-User-Email: ops-owner@example.com'

curl -s -X POST "$BASE/api/v1/approvals/$APPROVAL_ID/approve" \
-H 'X-User-Email: approver@example.com' | jq

curl -s $BASE/api/v1/dashboard/readiness-score | jq

EXPORT_ID=$(curl -s -X POST "$BASE/api/v1/exports" \
-H 'Content-Type: application/json' \
-H 'X-User-Email: approver@example.com' \
-d '{"format":"csv"}' | jq -r .export_id)

curl -s "$BASE/api/v1/exports/$EXPORT_ID/download"
```

## User roles

- `analyst@example.com`: imports data, assigns exceptions, reviews dashboard
- `ops-owner@example.com`: owns remediation, retires obsolete SSIs, submits critical changes
- `refdata@example.com`: reference-data owner
- `approver@example.com`: approves or rejects pending settlement-critical changes
- `risk@example.com`: waives or reviews exceptions
- `admin@example.com`: seed administrator

Authentication is a stub: set `X-User-Email` to select the actor.

## Rule catalogue

| Rule ID | Severity | Purpose |
| --- | --- | --- |
| `SSI.REQUIRED.MARKET_FIELDS` | critical | Active-like SSIs require market, country, asset class, and settlement method |
| `SSI.REQUIRED.EFFECTIVE_FROM` | high | Active-like SSIs require an effective-from date |
| `SSI.GOVERNANCE.OWNER_REQUIRED` | high | Active-like SSIs require an accountable owner |
| `SSI.FORMAT.BIC` | high | BIC syntax must be 8 or 11 uppercase alphanumeric characters |
| `SSI.FORMAT.COUNTRY_CODE` | medium | Country code must be two letters and present in the synthetic directory |
| `SSI.FORMAT.CURRENCY_CODE` | medium | Currency code must be three letters and present in the synthetic directory |
| `SSI.DATE.INVALID_RANGE` | high | `effective_to` cannot be earlier than `effective_from` |
| `SSI.DUPLICATE.ACTIVE` | critical | Active-like SSIs cannot overlap in the same duplicate context |
| `SSI.STALE.12M` | high | Last confirmation older than 365 days is stale |
| `SSI.STALE.CRITICAL_MARKET` | critical | Stale SSI in FR, DE, GB, IT, or ES is critical |
| `SSI.T1.RECENT_CONFIRMATION` | high | T+1 markets need confirmation within 180 days |
| `SSI.T1.NO_CRITICAL_EXCEPTIONS` | critical | T+1 records must not have unresolved critical exceptions |

BIC validation is syntax-only and synthetic-demo scoped. It is not a current production BIC directory check.

## Readiness formula

```text
score = 100
- min(25, critical_exceptions * 1.25)
- min(20, high_exceptions * 0.20)
- min(10, medium_exceptions * 0.03)
- min(15, stale_ssis / max(total_ssis, 1) * 100)
- min(15, duplicate_active_ssis * 0.50)
- min(10, missing_owner * 0.20)
- min(10, approval_sla_breaches * 0.30)
- min(10, missing_audit_evidence * 0.50)
score = max(0, round(score, 1))
```

Bands:

- 90-100: Green
- 75-89: Amber
- 50-74: Red
- 0-49: Critical

## Private data-lab note

Real SSI data must stay private and authorized. This public repository uses synthetic data only. Raw SSI records, account numbers, client names, proprietary workflows, credentials, and reversible redaction maps must never be committed. External model calls must not contain raw SSI records or proprietary post-trade data. See `docs/private_data_lab_methodology.md`.
Empty file.
23 changes: 23 additions & 0 deletions apps/ssi-control-tower/app/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from __future__ import annotations

from typing import Any

from sqlalchemy.inspection import inspect


def as_dict(obj: Any) -> dict[str, Any]:
return {column.key: getattr(obj, column.key) for column in inspect(obj).mapper.column_attrs}


def rules_dict(rule: Any) -> dict[str, Any]:
return {
"rule_id": rule.rule_id,
"name": rule.name,
"severity": rule.severity,
"version": rule.version,
"enabled": rule.enabled,
"applies_to": rule.applies_to,
"evaluator": rule.evaluator,
"message": rule.message,
"suggested_fix": rule.suggested_fix,
}
Loading
Loading