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/WARN/INSUFFICIENT/DENY, the live server's ownAUDIT_DECISION_CLASSES— verbs.rs:389); raisesInvalidAuditDecisionErrorfor a bogus value, before the request. CLEANLIB-976 (0.5.1): this list previously omittedWARN, rejecting a real, live, returnable server decision value client-side; also corrected the docstring, which claimed to mirror acleanlib-cliconstant (VALID_DECISION_FILTERS) that CLEANLIB-879 had already removed.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.
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_advisoriesalone now defaults to the cleanapp customer-boundary facade —GET /v1/customer/enrich/advisories/{eco}/{lib}— independent ofbase_url, which the other 7HttpEnrichClientverbs keep using againstDEFAULT_ENRICH_BASE_URL(they are not on the facade's route whitelist:vector/vulnerability/advisories/remediationonly).DEFAULT_ADVISORIES_BASE_URL(https://cleanapp.clnstrt.dev) — the new default, constructor paramadvisories_base_url=.advisories_facade: bool = True—Falserestores the pre-fix direct/api/v1/advisories/{eco}/{lib}path againstadvisories_base_url=DIRECT_ENRICH_BASE_URLfor 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)
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's
get_advisorieswas 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 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.
Metadata
Release files for cleanlib-sdk 0.5.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| cleanlib_sdk-0.5.3.tar.gz | 75.0 kB | Without provenance | Details for cleanlib_sdk-0.5.3.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| cleanlib_sdk-0.5.3-py3-none-any.whl | Python 3 | none | any | Details for cleanlib_sdk-0.5.3-py3-none-any.whl |
Total release size: 163.0 kB
Release files / cleanlib_sdk-0.5.3.tar.gz
| Download URL | cleanlib_sdk-0.5.3.tar.gz |
|---|---|
| Size | 75.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0dba96706014f12dcb0fd65ca2ef77cfa6f2e1b455c54b62b22a34b6c80c36f3
|
|
BLAKE2b-256 checksum How to use checksums |
5f6dfd11efbdb8aed9f2ee7445fa2eb52856c59488cb0037f35c7bc88a781a88
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.6
|
Release files / cleanlib_sdk-0.5.3-py3-none-any.whl
| Download URL | cleanlib_sdk-0.5.3-py3-none-any.whl |
|---|---|
| Size | 88.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6cdc6d02d082db77e08bd975068cb56c119e9a4dcf2d7f711683f6a2d680ec2f
|
|
BLAKE2b-256 checksum How to use checksums |
288b282374c57e1af95d4943e018cd2d2f5aab8cddd769b24974d5278d5d1730
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.6
|
Security for cleanlib-sdk 0.5.3
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.