Skip to main content

cleanlib-sdk

CleanLibrary Python SDK — asyncio + httpx; mirrors the Rust cleanlib-client HTTP surface and the JavaScript @cleanstart/cleanlib-sdk sister-shape.

Status: v0.4.7 — production-ready.

Install

pip install cleanlib-sdk

Requires Python 3.10+. Depends on httpx>=0.27 and pydantic>=2, plus cryptography>=42 and rfc8785>=0.1.4 (gate-3 attestation verification, CLEANLIB-833/834).

Overview — per-domain client triad (v0.4.2+)

v0.4.2 split the SDK into three per-domain HTTP clients, each pointed at its own CleanLibrary surface:

Client Endpoint (default) Purpose
HttpVerdictClient https://cleanapp.clnstrt.dev App customer-verdict surface — fetch_verdict / scan / audit / policy_preview / risk_accept
HttpRemediationClient DEFAULT_REMEDIATION_BASE_URL (https://cleanapp.clnstrt.dev — customer facade; DIRECT_ENRICH_BASE_URL + facade=False for internal) Sparse 7-block /v1/customer/remediation responses
HttpEnrichClient DEFAULT_ENRICH_BASE_URL (https://cleanlib-enrich.clnstrt.dev) 8-verb cleanlib-enrich cascade — get_exploitability / get_exploitability_bulk / get_epss / get_kev / get_advisories / get_enrich / get_library_verdict / bulk_packages_check

The legacy Client (flat-method) still ships for source-compat with v0.4.0 callers and emits DeprecationWarning on __init__; scheduled removal in v1.0.0. New integrations should use the per-domain triad.

Usage — verdict client

import asyncio
from cleanlib_sdk import HttpVerdictClient, PolicyDenyError, RiskAcceptanceRequiredError

async def main() -> None:
    async with HttpVerdictClient(
        base_url="https://cleanapp.clnstrt.dev",
        api_key="clk_std_...",   # opaque CleanLibrary access key
    ) as c:
        try:
            v = await c.fetch_verdict("npm", "lodash", "4.17.21")
            print(f"{v.decision} composite_score={v.composite_score}")
            print(f"reasoning: {v.reasoning}")
        except PolicyDenyError as e:
            print(f"DENIED [{e.reason_code}]: {e.message}")
        except RiskAcceptanceRequiredError as e:
            print(f"RISK ACCEPT REQUIRED: {e.message}")
            if e.docs_url:
                print(f"see: {e.docs_url}")

asyncio.run(main())

Usage — enrich cascade

import asyncio
from cleanlib_sdk import (
    HttpEnrichClient,
    epss_found, kev_found,
    EXPLOITABILITY_BULK_CAP,
)

async def main() -> None:
    async with HttpEnrichClient(api_key="clk_std_...") as e:
        # KEV / EPSS return a discriminated union — one of *_found / *_not_found.
        kev = await e.get_kev("CVE-2021-44228")
        if kev_found(kev):
            print(f"KEV listed: due_date={kev.due_date}")

        epss = await e.get_epss("CVE-2021-44228")
        if epss_found(epss):
            print(f"EPSS score={epss.score} percentile={epss.percentile}")

        # Bulk: server caps at EXPLOITABILITY_BULK_CAP (100) per request.
        exp = await e.get_exploitability_bulk([
            "CVE-2021-44228", "CVE-2022-22965", "CVE-2023-4863",
        ])
        for cve in exp.results:
            print(cve.cve_id, cve.availability)

asyncio.run(main())

Usage — remediation

import asyncio
from cleanlib_sdk import HttpRemediationClient

async def main() -> None:
    async with HttpRemediationClient(api_key="clk_std_...") as r:
        rem = await r.get_remediation("npm", "lodash", "4.17.15")
        # Sparse 7-block: any block may be absent (RemediationOrAbsent).
        if rem.recommended_version is not None:
            print(f"upgrade to {rem.recommended_version.version}")

asyncio.run(main())

Usage — gate: verdict() / enforce() (CX-8)

Two ways to consume an assessment. A blocked verdict is a successful assessment, not an error — only couldn't get a verdict is an exception, so a transport/coverage failure can never be mistaken for "no findings, proceed" ([Absence≠safe]).

from cleanlib_sdk import (
    verdict, enforce, CustomerState,
    GateBlocked, GateNotAssessed, CoverageIncompleteError,
)

# `outcome` is what your acquisition produced: a CustomerState (you got a
# verdict of some tier) or a CleanLibraryError (you could not).

# 1. verdict() — RETURNS style. A Block is a VALUE, never an exception.
a = verdict(CustomerState.MALICIOUS)          # a completed assessment
print(a.state.as_str, a.tier, a.exit_code, a.is_allowed)   # malicious Tier.BLOCK 1 False

# The "not assessed" path is still a returned value — it is NOT clean:
na = verdict(CustomerState.NOT_YET_ASSESSED)
assert not na.is_allowed and na.exit_code == 2      # warn-tier, fail-closed

# A couldn't-get-a-verdict is the ONLY thing verdict() raises on:
try:
    verdict(CoverageIncompleteError("3 of 40 coordinates unreachable", "SCAN_ABORTED"))
except CoverageIncompleteError:
    ...   # a real failure — never silently a clean result

# 2. enforce() — RAISES style for CI gates. Returns None ONLY when clean.
try:
    enforce(CustomerState.CLEAN)              # -> None, proceed
    enforce(CustomerState.NOT_YET_ASSESSED)   # -> GateBlocked (warn, exit 2)
except GateBlocked as e:
    exit(e.exit_code)                         # 1 (block) or 2 (warn)
except GateNotAssessed as e:
    exit(e.exit_code)                         # fail-closed to 1 — a timeout /
                                              # coverage failure never exits 0

The same contract holds byte-for-byte in sdk-js (discriminated union), sdk-go (typed const + Valid()), and the Rust cleanlib-client reference — all assert against the shared CX8_GATE_EXPECTED.json conformance fixture.

Public API surface

Everything below is exported from the top-level cleanlib_sdk package and covered by __all__ (verified against dir(cleanlib_sdk) at release-cut per CLEANLIB-83). New symbols are grouped by the version they were introduced in.

0.5.0 — audit() decision + ecosystem filters (CLEANLIB-816/817)

AuditWindow only exposed since/until — the real server (cleanlib-app/src/verbs.rs) already validates and applies decision and ecosystem filters; verified live against prod before shipping this.

  • AuditWindow.decision: str | None / AuditWindow.ecosystem: str | None — new optional fields.
  • validate_audit_decision(value) -> None — case-insensitive check against AUDIT_VALID_DECISIONS (ALLOW/INSUFFICIENT/DENY, mirroring cleanlib-cli's VALID_DECISION_FILTERS); raises InvalidAuditDecisionError for a bogus value, before the request.
  • validate_audit_ecosystem(value) -> None — a deliberately separate, case-insensitive validator reusing the existing 8-ecosystem domain. NOT merged into validate_ecosystem, which stays case-sensitive — that is load-bearing for fetch_verdict/fetch_bytes (CLEANLIB-738: a case slip there silently reads as not_yet_assessed, not an error).
  • Both Client.audit() (deprecated) and HttpVerdictClient.audit() validate then append decision/ecosystem as query params alongside since/until.

0.5.0 — gate-3 attestation verification (CLEANLIB-833/834)

Python replication of the Rust reference implementation (cleanlib-client::attestation_verify, PR #537 on the cleanlib monorepo). Q14 posture: a capability, not a default — nothing else in this SDK calls these automatically.

  • verify_attestation(attestation_envelope, key_lookup) — verifies a SignedAttestation wire envelope's ECDSA P-256 signature over the RFC-8785 JCS-canonicalized attestation predicate. Pass the raw parsed-JSON dict (e.g. response.json()["attestation"]), not a SignedAttestation.model_dump() — see the module docstring for why. Raises AttestationInvalidError (malformed envelope / unknown key_id / genuine signature mismatch — always permanent) or TransportError (only via PubkeysEndpointLookup, transient).
  • PinnedKeyMap — the DEFAULT AttestationKeyLookup: a compiled-in key_id -> PEM map (today's staging + prod keys), fail-closed on an unknown key_id, zero network capability. with_extra_key(key_id, pem) pins an additional out-of-band-verified key.
  • PubkeysEndpointLookup — convenience-only helper that fetches GET /v1/pubkeys; its only sanctioned use is describe_unknown_key(key_id) -> str | None, a human-readable advisory that must never be treated as a trust decision. Do not wire this as verify_attestation's key lookup (see module docstring — this exact shape was PR #536's circular-trust defect, redesigned in PR #537).
  • AttestationKeyLookup — the lookup interface both of the above implement; a caller with a different trust-distribution mechanism (e.g. a pre-provisioned key bundle) can implement it directly.
  • Reuses the existing AttestationInvalidError (see the CLEANLIB-657/CX-8 entry below — same class, not redefined here); reason_code on the instances this module raises is one of the local ATTESTATION_* strings documented in the module docstring, distinct from the cross-SDK ReasonCode wire enum.

0.5.1 — get_advisories facade rebase (CLEANLIB-981, CLEANLIB-733 follow-up)

The v0.4.11 facade rebase (below) moved HttpRemediationClient onto cleanapp.clnstrt.dev but explicitly left HttpEnrichClient on the direct cleanlib-enrich.clnstrt.dev host as a follow-up. That gap meant every real customer's get_advisories call 404'd: customers only ever hold a CLEANLIB_API_KEY for cleanapp; the direct host needs the internal producer bearer, so the pre-fix hardcoded /api/v1/advisories/{eco}/{lib} path was simply absent on the host customers can actually reach.

  • get_advisories alone now defaults to the cleanapp customer-boundary facade — GET /v1/customer/enrich/advisories/{eco}/{lib} — independent of base_url, which the other 7 HttpEnrichClient verbs keep using against DEFAULT_ENRICH_BASE_URL (they are not on the facade's route whitelist: vector/vulnerability/advisories/remediation only).
  • DEFAULT_ADVISORIES_BASE_URL (https://cleanapp.clnstrt.dev) — the new default, constructor param advisories_base_url=.
  • advisories_facade: bool = True — False restores the pre-fix direct /api/v1/advisories/{eco}/{lib} path against advisories_base_url=DIRECT_ENRICH_BASE_URL for internal/producer-bearer callers.
  • The facade proxies this route byte-for-byte (no shape transform — the App-side dedup/object-shape logic applies only to /vulnerability, not /advisories), so the existing positional-array parser is unchanged.

v0.4.11 — customer-boundary facade rebase (CLEANLIB-733)

  • HttpRemediationClient now defaults to the cleanapp customer-boundary facade (DEFAULT_REMEDIATION_BASE_URL = https://cleanapp.clnstrt.dev), calling GET /v1/customer/remediation/{eco}/{pkg}. Customers authenticate with their own CLEANLIB_API_KEY (opens cleanapp only); cleanapp fetches from cleanlib-enrich with the producer bearer server-side.
  • DIRECT_ENRICH_BASE_URL (https://cleanlib-enrich.clnstrt.dev) — internal / producer-bearer callers bypass the facade with HttpRemediationClient(facade=False, base_url=DIRECT_ENRICH_BASE_URL, bearer=<producer>), which uses the pre-facade GET /api/v1/remediation/{eco}/{pkg} path.
  • Enrich cascade's get_advisories was rebased onto the facade in 0.5.0 (see above); its other 7 verbs + the CVE-keyed methods remain on the direct host (the merged facade whitelist is package-keyed vector/vulnerability/advisories/remediation only — those verbs aren't on it).

v0.4.10 — CX-8 verdict()/enforce() dual gate API (CLEANLIB-657)

  • verdict(outcome) -> Assessment — RETURNS style. A completed assessment of ANY tier (a Block is a value, never an exception) becomes an Assessment; a couldn't-assess propagates as its raised exception. outcome is a CustomerState (a verdict of some tier) or a CleanLibraryError (none could be obtained).
  • enforce(outcome) -> None — RAISES style for gates. Returns None iff clean-to-proceed; a non-clean completed assessment raises GateBlocked, a couldn't-assess raises GateNotAssessed. Fail-closed: NOT_YET_ASSESSED / RANGE_NOT_RESOLVED never pass the gate, so "not assessed" can never read as "allowed".
  • Assessment — a completed assessment wrapping a CustomerState (.state / .tier / .exit_code / .is_allowed / .enforce()).
  • GateError — base for gate refusals; GateBlocked (assessed → do not proceed) and GateNotAssessed (could not assess; fail-closed exit code) preserve the STOP-vs-FAILURE distinction.
  • CoverageIncompleteError / AttestationInvalidError — the couldn't-assess causes (mirror cleanlib-client CleanLibraryError::CoverageIncomplete / ::AttestationInvalid).

Mirrors cleanlib-client/src/gate.rs; all four SDKs assert against the shared CX8_GATE_EXPECTED.json conformance fixture (tests/test_cx8_gate_contract.py).

v0.4.11 — typed Ecosystem + client-side validation (CLEANLIB-738)

  • Ecosystem — enum of the 8 supported package ecosystems (npm / pypi / go / maven / crates / nuget / rubygems / composer); mirrors the CLI's shipped accepted set and the App SUPPORTED_ECOSYSTEMS const. Subclasses str (Ecosystem.NPM == "npm"). Same typed-uncertainty class as CustomerState (CX-8).
  • validate_ecosystem(value) -> str — returns the canonical string if supported, else raises UnknownEcosystemError. Wired into HttpVerdictClient.fetch_verdict / fetch_bytes so an unknown ecosystem (e.g. cargo for crates) is rejected client-side, before the request, instead of passing through to a silent not_yet_assessed ([Absence≠safe]).
  • ALL_ECOSYSTEMS — the canonical 8-tuple (the validator's source of truth).
  • UnknownEcosystemError — raised on an unsupported ecosystem; the message names the accepted set (mirrors the CLI).

v0.4.7 — Verdict.decision auto-derived

  • Verdict.decision is now auto-derived at the type layer (CLEANLIB-294) — consumers no longer need to re-run derive_status on a raw Verdict; the field is populated from the envelope substance + freshness precedence at parse time.

v0.4.6 — envelope emit

  • verdict_to_envelope_v1(verdict) -> dict — canonical Verdict→envelope packer per CLEANLIB-48. Sister-shape of the sdk-js emitter; the fixture-driven contract test in tests/verdict_to_envelope_contract.py keeps both SDKs byte-identical.

v0.4.5 — previous-verdict parity

  • PreviousVerdict — envelope carries the prior verdict when a re-scan changes the decision; enables audit-log diff view.

v0.4.4 — customer-state taxonomy (CLEANLIB-178)

  • CustomerState — enum mirroring cleanlib-client/src/customer_state.rs.
  • Tier — customer tier enum (FREE / STD / PRO / …).
  • ALL_VERDICT_SOURCES — canonical registry of verdict-source strings (used by cross-SDK contract test to guard against silent drift).

v0.4.3 — brand/metadata sanitize

Pyproject brand cleanup; no new symbols.

v0.4.2 — enrich cascade + F4 default-flip

Clients:

  • HttpVerdictClient — App customer-verdict surface (completes the triad; the v0.4.1 split shipped remediation + enrich).
  • HttpEnrichClient — 8-verb cleanlib-enrich cascade; defensive wrappers (advisories double-parse + dedup; cve_id ↔ cveId normalization; sparse-by-design tolerance).

Constants:

  • DEFAULT_ENRICH_BASE_URL — https://cleanlib-enrich.clnstrt.dev.
  • DEFAULT_REMEDIATION_BASE_URL — F4-flipped to https://cleanlib-enrich.clnstrt.dev (was the vva URL in v0.4.1). Consumers passing an explicit base_url= are unaffected.
  • EXPLOITABILITY_BULK_CAP — server-side cap on get_exploitability_bulk batch size.
  • REMEDIATION_CACHE_TTL_SECS — default TTL used by the remediation client's in-process cache.

Enrich wire-shapes:

  • AdvisoryRow, AdvisorySeverity — advisories.
  • AvailabilityFlag, ExploitabilityAvailability, ExploitabilityResponse — exploitability triage.
  • EpssResponse, EpssOrNotFound, epss_found, epss_not_found — EPSS with found/not-found discriminated union + helpers.
  • KevResponse, KevOrNotFound, kev_found, kev_not_found — KEV with found/not-found discriminated union + helpers.
  • EnrichResponse, LibraryVerdictResponse — top-level cascade responses.
  • BulkPackageRef, BulkPackageResult, BulkPackagesCheckResponse — bulk_packages_check request/response shapes.

v0.4.1 — contract enforcement + remediation client

  • HttpRemediationClient — sparse 7-block /remediation responses.
  • RemediationBlock, RemediationResponse, RemediationOrAbsent — remediation wire-shapes.
  • ReasonCode — 15-entry canonical registry (Enum).
  • ALL_REASON_CODES — read-only tuple of every registered reason code.
  • VERDICT_ENVELOPE_V1_SCHEMA, SCHEMA_ID, STATUS_ENUM, AVAILABILITY_ENUM — JSON-Schema mirror + enum sets.
  • derive_status(envelope) -> StatusResult — canonical algorithm per App dispatch §4 binding contract (substance precedence + freshness override).
  • StatusResult — output shape of derive_status.
  • Client — legacy flat-method class; emits DeprecationWarning on __init__ (v1.0.0 removal).

v0.4.0 — verb cascade + rich-data ripple

Response types shared across the triad:

  • Verdict — canonical verdict envelope with attestation.
  • Attestation, SignedAttestation — signed provenance.
  • RecommendedVersion, PackageRisk, PackageRef — rich-data fields.
  • ScanResult, ScanResponse — scan endpoint.
  • AuditWindow, AuditWindowResponse — audit endpoint.
  • PolicyPreviewResult, PolicyPreviewResponse — policy preview.
  • RiskAcceptResponse — risk-accept submission response.

Sub-modules

Everything the flat surface re-exports also lives in a namespaced sub-module — import from either. Use the sub-module path when you need to disambiguate against a same-named symbol from a sister SDK.

  • cleanlib_sdk.attestation_verify — gate-3 verify_attestation + PinnedKeyMap / PubkeysEndpointLookup / AttestationKeyLookup / AttestationInvalidError (CLEANLIB-833/834, PR #537 reference).
  • cleanlib_sdk.client — legacy Client (deprecated).
  • cleanlib_sdk.customer_state — v0.4.4 state taxonomy.
  • cleanlib_sdk.derive_status — derive_status + StatusResult.
  • cleanlib_sdk.errors — every exception in the hierarchy below.
  • cleanlib_sdk.http — the three Http*Clients, wire-shapes, constants, helper functions.
  • cleanlib_sdk.reason_codes — ReasonCode, ALL_REASON_CODES.
  • cleanlib_sdk.schema — VERDICT_ENVELOPE_V1_SCHEMA, enum sets.
  • cleanlib_sdk.transport — internal httpx.AsyncClient wrapper (not re-exported at the top level; used by all three Http*Clients).
  • cleanlib_sdk.types — response dataclasses.
  • cleanlib_sdk.verdict_to_envelope — verdict_to_envelope_v1.

Error hierarchy

All errors descend from CleanLibraryError. Subclasses:

Exception HTTP Triggered by
PolicyDenyError 403 / 451 POLICY_DENY_VERDICT / POLICY_DENY_RULE_EXPLICIT
IntegrityFailureError 403 INTEGRITY_FAILURE
RateLimitExceededError 429 tier-throttled; carries retry_after_seconds
RiskAcceptanceRequiredError 403 RISK_ACCEPTANCE_REQUIRED
AuthenticationError 401 / 403 KEY_INVALID / KEY_EXPIRED / KEY_SCOPE_INSUFFICIENT
InsufficientDataError 403 INSUFFICIENT_DATA_FAIL_CLOSED
PackageNotFoundError 404 not in catalog + ingest declined
ServerError 5xx retryable on 502/503/504
ProblemError any RFC 9457 application/problem+json (CLEANLIB-536); carries a ProblemDetails — branch on .problem.reason_class, honor .problem.retryable
ProblemDetails — the parsed RFC 9457 problem document (type/title/status/detail/instance + reason_class/retryable/self_healing/resolution)
TransportError — network / TLS / timeout / DNS
ParseError — response body shape mismatch

Development

pip install -e ".[dev]"
pytest
ruff check .

Contract tests (tests/contract.py, tests/customer_state_contract.py, tests/verdict_to_envelope_contract.py) load fixtures from cleanlib-contract-fixtures and assert byte-identical behavior against the sdk-js sister — do not skip these; a divergence is a wire-shape regression.

Cross-references

License

Proprietary — CleanStart.

Release files for cleanlib-sdk 0.5.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for cleanlib-sdk 0.5.1
File Size Uploaded
cleanlib_sdk-0.5.1.tar.gz 71.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for cleanlib-sdk 0.5.1
File Interpreter ABI Platform
cleanlib_sdk-0.5.1-py3-none-any.whl Python 3 none any Details

Total release size: 155.1 kB

Release files / cleanlib_sdk-0.5.1.tar.gz

Download URL cleanlib_sdk-0.5.1.tar.gz
Size 71.3 kB
Tags Source
SHA-256 checksum
How to use checksums
281cd5b2b81908d87b155192259489e8f87ede49c210300ad9bb8db80ddab39f
BLAKE2b-256 checksum
How to use checksums
e89682f0c88d486e0049c13119fa340e59c65c5eb1429f00035286dbfe33cfbc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.6

Release files / cleanlib_sdk-0.5.1-py3-none-any.whl

Download URL cleanlib_sdk-0.5.1-py3-none-any.whl
Size 83.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
89e2af551f1fed52024ef6a0568fb94b5f41a1b6a97ca0cb5fb1657b43d36937
BLAKE2b-256 checksum
How to use checksums
09afab9dd110c147d3e878ef51ddc648e002fec14329becb4fcc547c155cae3d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.6

Release history Release notifications | RSS feed

This release

0.5.1 This release

2 release files

0.5.0

2 release files

0.4.21

2 release files

0.4.20

2 release files

0.4.19

2 release files

0.4.18

2 release files

0.4.17

2 release files

0.4.16

2 release files

0.4.15

2 release files

0.4.14

2 release files

0.4.9

2 release files

0.4.8

2 release files

0.4.7

2 release files

0.4.6

2 release files

0.4.5

2 release files

0.4.4

2 release files

0.4.3

2 release files

0.4.2

2 release files

0.4.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page