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 againstAUDIT_VALID_DECISIONS(ALLOW/INSUFFICIENT/DENY, mirroringcleanlib-cli'sVALID_DECISION_FILTERS); raisesInvalidAuditDecisionErrorfor 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 intovalidate_ecosystem, which stays case-sensitive — that is load-bearing forfetch_verdict/fetch_bytes(CLEANLIB-738: a case slip there silently reads asnot_yet_assessed, not an error).- Both
Client.audit()(deprecated) andHttpVerdictClient.audit()validate then appenddecision/ecosystemas query params alongsidesince/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 aSignedAttestationwire envelope's ECDSA P-256 signature over the RFC-8785 JCS-canonicalizedattestationpredicate. Pass the raw parsed-JSON dict (e.g.response.json()["attestation"]), not aSignedAttestation.model_dump()— see the module docstring for why. RaisesAttestationInvalidError(malformed envelope / unknown key_id / genuine signature mismatch — always permanent) orTransportError(only viaPubkeysEndpointLookup, transient).PinnedKeyMap— the DEFAULTAttestationKeyLookup: a compiled-inkey_id -> PEMmap (today's staging + prod keys), fail-closed on an unknownkey_id, zero network capability.with_extra_key(key_id, pem)pins an additional out-of-band-verified key.PubkeysEndpointLookup— convenience-only helper that fetchesGET /v1/pubkeys; its only sanctioned use isdescribe_unknown_key(key_id) -> str | None, a human-readable advisory that must never be treated as a trust decision. Do not wire this asverify_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_codeon the instances this module raises is one of the localATTESTATION_*strings documented in the module docstring, distinct from the cross-SDKReasonCodewire enum.
v0.4.11 — customer-boundary facade rebase (CLEANLIB-733)
HttpRemediationClientnow defaults to the cleanapp customer-boundary facade (DEFAULT_REMEDIATION_BASE_URL = https://cleanapp.clnstrt.dev), callingGET /v1/customer/remediation/{eco}/{pkg}. Customers authenticate with their ownCLEANLIB_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 withHttpRemediationClient(facade=False, base_url=DIRECT_ENRICH_BASE_URL, bearer=<producer>), which uses the pre-facadeGET /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 anAssessment; a couldn't-assess propagates as its raised exception.outcomeis aCustomerState(a verdict of some tier) or aCleanLibraryError(none could be obtained).enforce(outcome) -> None— RAISES style for gates. ReturnsNoneiff clean-to-proceed; a non-clean completed assessment raisesGateBlocked, a couldn't-assess raisesGateNotAssessed. Fail-closed:NOT_YET_ASSESSED/RANGE_NOT_RESOLVEDnever pass the gate, so "not assessed" can never read as "allowed".Assessment— a completed assessment wrapping aCustomerState(.state/.tier/.exit_code/.is_allowed/.enforce()).GateError— base for gate refusals;GateBlocked(assessed → do not proceed) andGateNotAssessed(could not assess; fail-closed exit code) preserve the STOP-vs-FAILURE distinction.CoverageIncompleteError/AttestationInvalidError— the couldn't-assess causes (mirrorcleanlib-clientCleanLibraryError::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 AppSUPPORTED_ECOSYSTEMSconst. Subclassesstr(Ecosystem.NPM == "npm"). Same typed-uncertainty class asCustomerState(CX-8).validate_ecosystem(value) -> str— returns the canonical string if supported, else raisesUnknownEcosystemError. Wired intoHttpVerdictClient.fetch_verdict/fetch_bytesso an unknown ecosystem (e.g.cargoforcrates) is rejected client-side, before the request, instead of passing through to a silentnot_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.decisionis now auto-derived at the type layer (CLEANLIB-294) — consumers no longer need to re-runderive_statuson a rawVerdict; 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 intests/verdict_to_envelope_contract.pykeeps 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 mirroringcleanlib-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 ↔ cveIdnormalization; sparse-by-design tolerance).
Constants:
DEFAULT_ENRICH_BASE_URL—https://cleanlib-enrich.clnstrt.dev.DEFAULT_REMEDIATION_BASE_URL— F4-flipped tohttps://cleanlib-enrich.clnstrt.dev(was the vva URL in v0.4.1). Consumers passing an explicitbase_url=are unaffected.EXPLOITABILITY_BULK_CAP— server-side cap onget_exploitability_bulkbatch 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_checkrequest/response shapes.
v0.4.1 — contract enforcement + remediation client
HttpRemediationClient— sparse 7-block/remediationresponses.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 ofderive_status.Client— legacy flat-method class; emitsDeprecationWarningon__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—scanendpoint.AuditWindow,AuditWindowResponse—auditendpoint.PolicyPreviewResult,PolicyPreviewResponse—policy preview.RiskAcceptResponse—risk-acceptsubmission 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-3verify_attestation+PinnedKeyMap/PubkeysEndpointLookup/AttestationKeyLookup/AttestationInvalidError(CLEANLIB-833/834, PR #537 reference).cleanlib_sdk.client— legacyClient(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 threeHttp*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— internalhttpx.AsyncClientwrapper (not re-exported at the top level; used by all threeHttp*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
- CleanLibrary docs — customer documentation portal
- Rust SDK:
cleanlib-client— reference implementation - Go SDK:
cleanlib-sdk-go - JavaScript SDK:
@cleanstart/cleanlib-sdk
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)
| File | Size | Uploaded | |
|---|---|---|---|
| cleanlib_sdk-0.5.0.tar.gz | 68.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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
|