Building an extension
ImportSpy exposes small Python protocols in importspy.extensions. Implement the
documented methods; inheritance from a framework base class is not required.
| Extension | Method | Responsibility |
|---|---|---|
EvidenceProvider |
collect(context, dependencies) -> Iterable[Evidence] |
Collect facts; declare name and requires_network. |
PolicyValidator |
evaluate(policy, evidence, dependencies, context) -> Iterable[Violation] |
Evaluate collected facts without IO. |
RuntimeValidator |
validate(module, decision) -> Iterable[Violation] |
Inspect objects after explicit execution. |
Reporter |
render(decision) -> str |
Format a decision without rerunning checks. |
These objects are trusted application/plugin code. Loading a provider entry point executes that package's Python code. They must never import the inspected target during admission. Network declaration is an engine scheduling contract, not a sandbox that prevents a dishonest provider from opening sockets.
Complete offline provider
This provider describes the resolver's installed inventory. It deliberately emits an observation, not cryptographic provenance:
from collections.abc import Iterable
from importspy.domain import AdmissionContext, Dependency, Evidence
class InventoryProvider:
name = "example-inventory"
requires_network = False
def collect(
self, context: AdmissionContext, dependencies: list[Dependency]
) -> Iterable[Evidence]:
names = sorted({
dist.name
for dependency in dependencies
for dist in dependency.distributions
})
yield Evidence(
kind="example.inventory",
subject="dependencies",
observed={"distributions": [name for name in names]},
provider=self.name,
status="observed",
detail="Names copied from installed metadata; not provenance verification.",
)
Require it in a contract:
schema_version: 1
evidence_requirements:
- kind: example.inventory
subject: dependencies
provider: example-inventory
status: observed
Select the provider explicitly:
from pathlib import Path
from importspy.domain import AdmissionRequest
from importspy.engine import AdmissionEngine
engine = AdmissionEngine(providers=(InventoryProvider(),))
decision = engine.check(AdmissionRequest(
subject=Path("plugin.py"),
contract=Path("plugin.importspy.yml"),
))
print(decision.decision)
The tested checkout example contains this provider, a pure distribution allowlist validator, and runnable contracts. From the checkout:
It admits an observed packaging inventory, denies an application allowlist
mismatch, and denies missing provider evidence. No credentials, network, or
provenance claims are involved.
Add pure policy
The example's DistributionAllowlist reads the selected provider's inventory,
validates its shape, and compares names against an application-supplied allowlist.
It yields EXAMPLE-P001 for unusable inventory and EXAMPLE-P002 for a name outside
the allowlist. It performs no filesystem or network access.
from examples.admission.extensions import DistributionAllowlist, InventoryProvider
from importspy.engine import AdmissionEngine
engine = AdmissionEngine(
providers=(InventoryProvider(),),
validators=(DistributionAllowlist(frozenset({"packaging"})),),
)
examples is checkout code, not an installed ImportSpy module; put your own
implementation in your extension package. Custom validators run alongside core
policy and cannot erase existing violations. Use a unique machine-readable code
prefix and return source locations only when known.
Package a provider entry point
In your extension package's pyproject.toml:
[project.entry-points."importspy.evidence_providers"]
company-inventory = "company_inventory:InventoryProvider"
The value points to a zero-argument factory or class implementing the provider protocol. After installing the extension in the checking environment:
Only explicitly named providers are loaded. Unknown or duplicate entry-point names
are configuration errors. The entry-point name used on the command line can differ
from the provider's name; evidence pins use the provider's declared name.
Provider names must be unique, nonempty strings and must not start with
importspy, which is reserved for authoritative core evidence. The engine stamps
returned records with the selected provider's identity.
Policy validators, runtime validators, and reporters are passed as Python objects. Core does not auto-discover or register every installed extension, and the CLI does not currently offer arbitrary custom validator or reporter entry points.
Network, unavailable evidence, and caching
A network provider sets requires_network = True. Core skips its collection
unless AdmissionEngine(allow_network=True) or CLI --allow-network is selected.
A skipped or failed collector produces unavailable evidence. Required facts then
deny admission; optional missing facts alone do not.
Providers should use bounded requests, explicit endpoints, careful redirect and credential handling, and documented caches. Cache keys must include the identity of the artifact or fact, verification configuration, and freshness constraints. Core does not supply a provider cache or infer that cached verification is current.
Each collector's complete iterable is validated before its evidence is appended. If collection raises or yields invalid objects, its partial results are discarded. Do not encode a failed verification as a truthy object accepted by a generic evidence requirement; emit false, unknown, or unavailable as appropriate.
Runtime validators and reporters
AdmissionEngine(runtime_validators=(validator,)).load(request) freshly checks
admission, executes the inspected target bytes, then calls the runtime validator.
The callback cannot prevent or roll back top-level effects. A runtime denial has
target_executed: true. An unknown static requirement must first be redesigned or
moved into an explicit runtime policy; load does not bypass a static denial.
To add output, implement Reporter.render(decision) and call it from your
application. Built-ins are HumanReporter, JSONReporter, and SARIFReporter.
Never repeat collection or load the target merely to format a decision.
See evidence semantics, supply-chain integration, and the community roadmap for contribution scopes.