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.

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 + the CVE-keyed methods remain on the direct host for now (their facade rebase is a follow-up; the merged facade whitelist is package-keyed vector/vulnerability/advisories/remediation only).

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.0

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.0
File Size Uploaded
cleanlib_sdk-0.5.0.tar.gz 68.7 kB Details

Built distribution (wheel)

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

Total release size: 150.0 kB

Release files / cleanlib_sdk-0.5.0.tar.gz

Download URL cleanlib_sdk-0.5.0.tar.gz
Size 68.7 kB
Tags Source
SHA-256 checksum
How to use checksums
c6a15d7ae4f7c9515ebbf7d81fa5c049871ae7b3eae8584ddffb5126c6ce4c64
BLAKE2b-256 checksum
How to use checksums
501644ac3e9c14ccc0234af239160415a55de18b7f0957c1d9fe8024374eebae
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.0-py3-none-any.whl

Download URL cleanlib_sdk-0.5.0-py3-none-any.whl
Size 81.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
526802fa71095acee970c10a8ef892ebea5cb116b1888ea9e56ea7644c759f30
BLAKE2b-256 checksum
How to use checksums
9705b9dd9896abcd9d08879a88d75e984d58b6986756cc25ac677a0aaac19034
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

0.5.1

2 release files

This release

0.5.0 This release

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