--- name: sumsub-conformance description: Check whether a tenant's DEPLOYED Sumsub config actually satisfies a regulation/policy document — tracing each requirement to where it is collected, scored, and ENFORCED, and flagging "collected-but-not-enforced" gaps. TRIGGER when the user has a regulation/policy/requirements doc (PDF or text) and wants to verify the live config matches it, audit a client's setup against compliance rules, "does my config satisfy this regulation", "check conformance / gap analysis", or close the loop after configuring with the create-* skills. SKIP for building config (sumsub-create-*) or for generating a config plan from a regulation (sumsub-analyze-regulation). For pure hygiene linting with no regulation, run this skill's bundled lint_config.py sub-pass directly. allowed-tools: Read, Write, Bash --- # Sumsub — Conformance (regulation ↔ deployed config) Traces every requirement of a regulation through the **live** config graph and answers, per requirement: **collected? → scored? → enforced?** The headline finding it surfaces — that nothing else does — is **COLLECTED-BUT-NOT-ENFORCED**: the config gathers and scores the data a regulation needs, then doesn't act on it (e.g. an appropriateness questionnaire that's scored but never routed). This is an **advisory gap assistant, not a compliance guarantee.** It verifies the config-representable parts deterministically and honestly delimits the rest (semantic intent = judgment; SDK/platform outcomes = out of scope). Get a human to sign off. ## When to use - Existing client: "here's our regulation, here's our tenant — what's actually enforced?" - New client who configured Sumsub with the create-* skills: close the **generate → verify** loop — did the assembled config end up enforcing the regulation it was built from? ## Endpoints (read-only) All via `scripts/sumsub_curl.sh` (App-Token HMAC). `resolve_graph.sh` wraps these: | Purpose | Method + Path | |---|---| | Levels (find target) | `GET /resources/applicants/-/levels` | | Workflows (auto-find the one referencing the level) | `GET /resources/api/applicantWorkflows` + `GET /resources/api/applicantWorkflows/{id}` | | Questionnaire **content + scores** | `GET /resources/api/questionnaires/{id}` | | PoA / cross-check preset | `GET /resources/api/poaStepSettings/{id}` · `GET /resources/api/crossCheckPresets/{id}` | | Entitlements (`allowedChecks` keys) | `GET /resources/api/agent/settings/bgCheckTargets` | ## Auth — App Token + secret (sandbox only) This skill talks to the public Sumsub API and signs each request per [the authentication reference](https://docs.sumsub.com/reference/authentication). The how-it-works writeup lives in [`sumsub-api-auth`](../sumsub-api-auth/SKILL.md). > **⚠️ Sandbox tokens only.** Read-only, but still: do not accept a production App Token. If the user > offers one, refuse and ask for a sandbox pair at > (**Connect Sumsub to your AI agent** -> > **Build & configure** -> **Generate token**). The helper > rejects tokens that don't start with `sbx:`. | Var | Example | |---|---| | `SUMSUB_APP_TOKEN` | `sbx:...` | | `SUMSUB_SECRET_KEY` | paired secret | | `SUMSUB_BASE` / `SUMSUB_ENV=dev` | optional; `dev` targets `dev-api.sumsub.com` (internal). | ## Procedure > **Use the scripts — do not hand-roll the deterministic steps.** The whole point of this skill is > that resolution and the verdict are deterministic, not re-derived by eyeball each run: > - **Resolve the graph ONLY via `resolve_graph.sh`.** Do **not** call `/resources/...` endpoints > directly, list levels by hand, or strip the `HTTP ` trailer yourself — the script does all > of that and emits one clean graph. > - **If the graph can't be resolved, STOP — never work around the failure.** A `401`/`403`, a `404`, > or `0 levels` / "level not found" from `resolve_graph.sh` means the run cannot proceed. Report the > failure plainly (likely a bad/non-sandbox token or wrong level name) and stop. Do **not** edit > `resolve_graph.sh` (or any script) to route around the error, add a by-id fallback, retry a > different endpoint, or otherwise improvise a graph — and **never** emit a report, bucket counts, or > a CONFORMANT verdict from a partial or substituted graph. No resolved graph → no findings. > - **Derive the verdict ONLY from `trace.py` / `report.py`.** Do **not** read the workflow/questionnaire > JSON by eye and decide "enforced / not enforced" yourself — that re-introduces variance and > hallucination. The `COLLECTED-NOT-ENFORCED` finding must come from the engine. > - **If a script already ran, trust its output — do not re-verify it with manual API calls.** > - **To inspect the graph for the Map step, run `python3 scripts/graph_summary.py "$CONFIG"`.** Do > **not** hand-iterate the graph JSON: `questionnaires`/`poaPresets`/`crossCheckPresets` are **dicts > keyed by id** (iterating yields id strings → `'str' has no attribute 'get'`) and `entitlements` > is a **list**. `graph_summary.py` prints the section/item indices + scores you need, correctly. > The agent's judgment belongs to two steps only: ingest (reading the regulation) and map (matching > requirements to deployed config elements — questions, docSets, fields, countries, entitlements). > Everything else is the scripts. > **Artifacts — MANDATORY location & naming.** Every run MUST write its artifacts into the **same > directory as the source regulation file**, named from its **stem** (filename without extension). > Never save in the cwd or under improvised names. Derive the exact paths once, up front: > ``` > eval "$(python3 scripts/artifact_paths.py "")" > # $REQUIREMENTS = /_regulations.json (the requirements you build in steps 1+3) > # $CONFIG = /_config.json (resolve_graph output, step 2) > # $REPORT = /_report.md (the rendered report, step 6) > ``` > e.g. `Level4regulations.pdf` → `Level4regulations_regulations.json`, `Level4regulations_config.json`, > `Level4regulations_report.md`. Use `$REQUIREMENTS` / `$CONFIG` / `$REPORT` verbatim in the steps below. 1. **Ingest the regulation** (judgment). **Just Read the document with the Read tool — it reads PDFs natively (renders pages + extracts text), so nothing needs to be installed.** For a PDF over ~10 pages, Read it in page ranges (the Read tool's `pages` parameter, ≤20 pages per call). Reading visually is equal or better than a text extractor — it preserves tables, headings and layout that flat-text extraction loses. *Optional, only if already installed and never a blocker:* `pypdf`/`pdftotext` can pull a long, text-heavy PDF in one shot, and `scripts/render_regulation.py` (PyMuPDF) pre-renders a scanned/image-only PDF to PNGs for explicit DPI control — see [`references/conformance-method.md`](references/conformance-method.md) step 1. If a library is absent, just Read the PDF directly. Then extract requirements and write them to **`$REQUIREMENTS`** (the `_regulations.json` beside the source — see the Artifacts rule above), per [`references/requirements-schema.md`](references/requirements-schema.md): each = `{id, source, text, type, enforceable, …}`. **Always capture `source`** — the citation into the original document (section number / heading / page, e.g. `"§5.9.2"`, `"Section 2 — Scoring Result"`, `"p.21"`); the report leads with it so findings trace back to the regulation, not to an artificial id. Mark outcomes that live in SDK/platform/CRM as `enforceable: sdk|platform`. **Show the extracted requirements to the user and confirm before tracing** — this is the highest-risk step. 2. **Resolve the deployed graph** (deterministic). `bash scripts/resolve_graph.sh > "$CONFIG"` (level + questionnaires-with-scores + presets + auto-found workflow + entitlements). **This one command replaces any manual level-listing / curl / HTTP-trailer stripping — don't do those by hand.** 3. **Map** (judgment). **First inspect the graph: `python3 scripts/graph_summary.py "$CONFIG"`** — it prints questionnaires with `[section]/[item]` indices + option scores, docSets, APPLICANT_DATA fields, geo, entitlements, and the workflow (don't hand-iterate the JSON). Then for each requirement pick the right `type` and fill its `match` — **not everything is a questionnaire question.** The per-type `match` shapes are in [`references/requirements-schema.md`](references/requirements-schema.md): - questionnaire question/score → `match: {questionnaire, section, item}` (+ `expectScores`); - document collected → `match: {docSet: "IDENTITY", types: [...]}`; - **applicant-data field present** → `type: data-field`, `match: {field: "dob"}` / `{fields: [...]}`; - country allowlist / prohibited jurisdictions → `type: country-eligibility`, `match: {mustHaveAllowlist: true}` / `{mustExclude: [...]}`; - selfie liveness → `type: liveness`, `match: {}`; - capability provisioned → `type: entitlement`, `match: {entitlement: "WATCHLISTS"}`; - screening/routing outcome → set a **discriminating** `enforcementSignal` (NOT a generic token). Be conservative: if unsure a thing exists, leave `match: null` (→ MISSING) rather than guess. 4. **Trace** (deterministic). `python3 scripts/trace.py --requirements "$REQUIREMENTS" --graph "$CONFIG"` → findings bucketed **MISSING / SCORING-MISMATCH / COLLECTED-NOT-ENFORCED / NOT-CONFIG-REPRESENTABLE / CONFORMANT**, each with evidence + a `basis` (deterministic vs judgment). **The bucket verdict comes from here — do not decide "enforced / not enforced" by reading the workflow JSON yourself.** 5. **(Optional) hygiene sub-pass.** Pipe the same graph through the bundled linter to catch silent misconfigurations (entitlement gaps, dangling preset/questionnaire refs, deprecated SDK, invalid AML provider) alongside conformance — entitlements are read straight from the graph: `bash scripts/resolve_graph.sh | python3 scripts/lint_config.py`. Rule catalog in [`references/lint-rules.md`](references/lint-rules.md) (generated from `references/rules.json`). 6. **Report — render with the canonical formatter to `$REPORT`, do NOT hand-format.** The format is fixed so every run looks identical: `python3 scripts/trace.py --requirements "$REQUIREMENTS" --graph "$CONFIG" | python3 scripts/report.py --date > "$REPORT"` → Markdown with a headline status, a fixed bucket-count table, findings grouped in fixed order (each with evidence + `basis`), and a fixed caveats footer. Present `$REPORT` verbatim. Do not re-summarise it in prose — that's what made past runs differ. (The `trace.py` JSON is the other canonical format for tooling — save it beside the others if a machine consumer needs it.) ## What it does NOT do (say it) - Not a compliance guarantee or a replacement for a compliance officer. - Does not verify **semantic** correctness beyond presence + scores + routing-enforcement (e.g. whether a question's wording is legally adequate is judgment). - Cannot verify SDK/platform/CRM outcomes (disclosure pop-ups, demo accounts, read-only mode, the 25%/10% restriction, CRM timestamps) — these are bucketed `NOT-CONFIG-REPRESENTABLE`. ## Guardrails - Keep **deterministic facts separate from judgment** (the `basis` field). Never present a mapping guess as a verified finding. - The trace walks **requirements → deployment only** — it never flags a deployed question as "extraneous" (avoids the false-positive class). Report extra questions, if at all, as an explicit advisory note. - `enforcementSignal` must be **discriminating** — a generic token (`country`) matches catch-all edges and yields false CONFORMANT. See [`references/requirements-schema.md`](references/requirements-schema.md). - **Fail honestly — never work around a resolution failure.** If `resolve_graph.sh` returns an auth error / 404 / `0 levels`, stop and report it. Do not edit the scripts to bypass it or fabricate a report from a partial graph (see the Procedure callout). ## See also - [references/requirements-schema.md](references/requirements-schema.md) — the requirement model + buckets. - [references/conformance-method.md](references/conformance-method.md) — the five steps, guardrails, scope. - [references/lint-rules.md](references/lint-rules.md) — the bundled hygiene-lint rule catalog (sub-pass). - [`sumsub-analyze-regulation`](../sumsub-analyze-regulation/SKILL.md) — the opposite direction (regulation → build plan). ## Worked examples (`examples/`) - `sample-requirements.json` + `sample-graph.json` — synthetic, exercise every bucket (drive `test_trace.py`). - `fiu-india-vasp-mapped.json` + `vasp-graph.json` — a broad AML guideline mapped against a hypothetical VASP level; **self-contained, trace them together offline**: `python3 scripts/trace.py --requirements examples/fiu-india-vasp-mapped.json --graph examples/vasp-graph.json | python3 scripts/report.py`. - `cfd-requirements.json` — real CFD/MiFID requirements (run against a live `cfd-onboarding-eu` graph). - `fiu-india-requirements.json` — the ingest-stage output for that broad guideline (before mapping). - `audit-input{,-dirty}.json` — hygiene-linter fixtures (drive `test_lint.py`).