cs-image-system-contract
The plugin contract: the one package a plugin is written against. It
carries the protocols a model and a builder satisfy, the constants and
field helpers a model is declared with, the lifecycle phases a hook keys
on, and the data carriers a hook returns. It depends on no other package of
the system -- pydantic and packaging only -- and the core
(base) depends on it, never the reverse: the contract
is upstream, the core conforms to it, and a new hook is a contract change
first. The core's base classes are the convenient implementation of the
contract, not the contract itself; a class that satisfies the protocols
structurally is a first-class plugin, and
testing.assert_conforms is the
check that says so. Stage 30 made the package (2026-10-29); until then the
protocols lived inside base, imported the registry, and a plugin's
pyproject.toml depended on the CLI host.
One module per kind of builder, over a shared core:
| Module | Carries |
|---|---|
core |
PluginArtifactProtocol (csis_name, csis_classifier), NameTypedProtocol, PluginMetadataProtocol, BuilderModelProtocol and RuntimeEnabledBuilderModelProtocol (what the core reads of any builder model), BuilderProtocol (the six universal hooks, the three finalize-phase hooks, finalize, the paths), and the three markers the orchestrator dispatches on: SelfInjectedNameProtocol, SubItemOverrideProtocol, ParentPropertyHoldingProtocol. |
runtime, operating_system, storage, image, instance, modification, group, user, state |
One <Kind>BuilderModelProtocol and one <Kind>BuilderProtocol per kind, with exactly the attributes and hooks the core reads. state also carries the backend seam: StateLocation, BackendRegistration, BackendKindProtocol and StateBackendModelProtocol. image carries the provider-specific image's two enums. |
metadata |
PluginMetadata, the record a plugin's entry point returns. |
constants |
VCT (the classifications), the sentinels (DEFAULT, SELF, NONE, OOPS_DEFAULTS), the field metadata keys, PLUGIN_TYPES (the entry-point groups), the collection names, the file names a run writes. |
model_config |
CSIS_MODEL_CONFIG, the one pydantic configuration every model is declared under (unknown keys are refused). |
fields, field_kinds |
fk_field, templated_field, deferred_list_field, deferred_list_fk_field, parent_field; the open registry of field kinds a plugin may extend. |
lifecycle, lifecycles |
ExecutionLifecyclePhase; Lifecycle, LifecycleSpec and the phases and builder classifications each built-in lifecycle owns. |
assets, executables |
Asset, AssetSet (what generate_items_* returns); ExecutableProtocol, CFExecutables (what get_commands_to_run_* returns). |
names, hcl |
safe_name, super_safe_name; HclRaw, hcl_expr, render_module_call. |
testing |
assert_conforms(cls, protocol) and conformance_problems. |
What moved where
Stage 30 moved these from the core into the package. Every old path but
cs_image_system.base.protocols still resolves: the module that held a
name re-exports it from here (the shims), kept on purpose while the
package is provisional. Removing the shims is the commitment to the
contract, and is done together with moving the package to a repository
of its own; until then an undo of the stage is a swap of which side
holds the bodies, with no import rewritten anywhere.
| Was | Is now |
|---|---|
base.constants (all of it: VCT, the sentinels, the field metadata keys, PLUGIN_TYPES, the collection and file names) |
contract.constants |
base.models.model_config.CSIS_MODEL_CONFIG |
contract.model_config |
base.helpers.field_helpers (fk_field, templated_field, deferred_list_field, deferred_list_fk_field, lookup_dataclass_field) |
contract.fields, plus the new parent_field |
base.helpers.field_kinds (the field-kind registry) |
contract.field_kinds |
base.lifecycle.ExecutionLifecyclePhase |
contract.lifecycle |
base.lifecycles: Lifecycle, LifecycleSpec, LifecycleLike, LIFECYCLE_ORDER, LIFECYCLE_PHASES, LIFECYCLE_BUILDER_VCTS (the registry functions stay in base) |
contract.lifecycles |
base.basic.asset: Asset, AssetSet (sort_and_write now takes the generation path) |
contract.assets |
base.models.executable_adds.CFExecutables (now generic over ExecutableProtocol) |
contract.executables |
base.protocols.plugin_metadata: PluginArtifactProtocol, PluginMetadataProtocol; base.protocols.name_typed_protocol: NameTypedProtocol, SelfInjectedNameProtocol, SubItemOverrideProtocol; base.protocols.builder_model_protocol: BuilderModelProtocol; base.protocols.parent_property_holding_protocol: ParentPropertyHoldingProtocol (deleted, no shim; their default bodies are base.models.mixins and the base classes) |
contract.core |
base.protocols.state_management.StateManagementRootProtocol (zero implementers) |
deleted |
base.utils.safe_name, super_safe_name |
contract.names |
base.utils.HclRaw, hcl_expr, render_module_call |
contract.hcl |
base.models.provider_specific_image.PSIState, PSISourceKind, psi_key |
contract.image |
hashicorp_utils.collector.StateLocation, BackendRegistration, the BackendKind protocol (still importable from the collector) |
contract.state (BackendKindProtocol; BackendWithoutKind is new) |
| nothing: new | contract.metadata.PluginMetadata, the kind protocols, contract.testing |
The protocols carry no implementation. Where a default body existed
(global_id, get_classification, get_self_injected_name, the parent
reference's registry lookup) it lives in the core's base classes and
mixins. The kind modules import core and only the kinds they declare they
read (runtime reads image for the provider-specific image's enums);
tests/test_v2_contract.py holds that
direction, holds the package to importing nothing from the core, and runs
assert_conforms over every base class and every built-in plugin's models
and builders.
Writing a plugin
A plugin is a Python distribution with an entry point in the group
cs_image_system.plugins.<kind> (PLUGIN_TYPES in constants: runtime,
state, modification, storage, group, user, image, os,
instance, other). The entry point loads to a zero-argument callable
that returns one PluginMetadata or a list of them.
- Depend on the contract. In
pyproject.toml,dependencies = ["cs-image-system-contract>=X"], withXthe first release that carries the contract (0.1.3 and before do not): a floor, not a pin -- the workspace's own packages pin each other at==because they release together, an external plugin does not. Addcs-image-system-baseonly if the plugin subclasses the core's base classes (every built-in plugin does; it is the convenient way, not the required one). Never depend oncs-image-system-system, the CLI host. - Write the model: a pydantic dataclass,
@dataclass(kw_only=True, config=CSIS_MODEL_CONFIG), that satisfies the kind's<Kind>BuilderModelProtocol. The universal fields arename,type(the field istype_, aliasedtype),description,aliases,executable,is_default,config,gitignore,tags, with theget_*readers andfinalize; the kind's module says what more the core reads. The class methodscsis_name()(the value a YAMLtype:names) andcsis_classifier()(VCT.<KIND>_BUILDER_MODEL) are how the registry resolves atype:to the class. A foreign key to another builder isfk_field(target=VCT....); a value the resolver renders istemplated_field(...); an item that belongs to a builder declares_model_id = parent_field(VCT....). - Write the builder: a class that satisfies the kind's
<Kind>BuilderProtocol: the universal surface inBuilderProtocol(generate_items_{before,during,after}return anAssetSet,get_commands_to_run_{before,during,after}aCFExecutables,pre_finalize_phase/post_finalize_phase/post_failed_phasefire around a phase's deferred commands,finalizeonce the configuration is complete) plus the kind's domain hooks, and the same two class methods,csis_classifier()returningVCT.<KIND>_BUILDER. The core'sBuilderBaseand<Kind>BuilderBaseimplement every hook with a default; subclass them to override only what the plugin does. - Return the metadata:
PluginMetadata(services={KEY: [Model, Builder]}, builders_for_models={Model: Builder}), whereKEYis thecsis_name. Item models a plugin injects under a classification go ininjected_models. - Prove it. In the plugin's tests,
assert_conforms(Model, <Kind>BuilderModelProtocol)andassert_conforms(Builder, <Kind>BuilderProtocol); then a drycs-image-system run --allover a tree that declares the plugin'stype:.
A state backend is the smallest kind: a model that answers
to_backend_registration() with a BackendRegistration naming its type's
BackendKindProtocol (where a workspace's state lives, the settings the
workspace's backend file carries, the settings a consumer's remote-state
data source carries), and a builder that emits nothing; the core hands
the registration to the terraform collector once the state backends are
loaded. The contract example plugin
is exactly that, written against this package alone with nothing of the
core, and the proof the bar runs; the
local state plugin is the same kind
written the convenient way on the base classes; the
dummy plugin is the template for a group or
user builder and for a hook plugin.
Not in the contract yet, so a plugin that needs them depends on base:
the run context (GlobalTypeContext, reached today through a base
builder's _get_context(); its protocol is a later stage), the
cs_image_system.plugins.hooks entry point (HookSet, typed on the
context) and the cs_image_system.bootstrap entry point (the bootstrap's
sections).
Prerequisites and integration
Nothing outside Python: the package needs Python 3.13 or later, pydantic
and packaging, and no account, tool, credential or network. A plugin
finds it by import (cs_image_system.contract.<module>); the core finds
it the same way and pins it at its own version.
Configuration reference
The contract reads no YAML of its own. What it declares about every
builder's entry, read by the core through BuilderModelProtocol:
| Field | Type | Default | Meaning |
|---|---|---|---|
name |
str | required | The builder's name; safe_name normalises it, display_name keeps what was written. Not a word in OOPS_DEFAULTS. |
type |
str | required | The plugin's csis_name, which the registry resolves to the model class; an alias resolves to the canonical name. |
description |
str or null | null | Free text. |
aliases |
set of str | empty | Other names the builder answers to. |
executable |
str or null | null | The executables: entry the builder runs, version-checked at validation; null for none. |
is_default |
bool | false | The default builder of its classification; one per classification. |
config |
mapping | empty | The plugin's own settings. |
gitignore |
list of str | empty | Entries the builder adds to the emitted .gitignore. |
tags |
mapping of str | empty | Tags every resource the builder emits carries. |
The kind modules say what more each kind reads (a runtime's defaults and
credentials, an OS builder's family and query, a user builder's email
templates); the plugin's own README is the reference for the fields it
adds. Unknown keys are refused by CSIS_MODEL_CONFIG (extra="forbid").
The sentinels default, self, none, the empty string and null
(OOPS_DEFAULTS) mean "not set" wherever a value is read, so nothing may
be named after one of them. Variations: none; the contract behaves the same
under a dry run and a real one, on every runtime.
What it tests and verifies
assert_conforms(cls, protocol)(testing): every member the protocol declares exists on the class, every method takes the protocol's parameters by name and kind in order (an implementation may add parameters with defaults, or take*argsand**kwargs), a property is a property or a dataclass field, a class method a class method. Annotations are not compared here; pyright does that where a class lists its protocol as a base (its override check,reportIncompatibleMethodOverride, is on as an error since hygiene XIX), and a protocol a class lists as a base is the question, not an answer, so its stubs never count. The core's tests/test_v2_contract.py runs it over every base class and every built-in plugin's models and builders atjust test; a plugin's own tests should run it over theirs. The verdict is anAssertionErrornaming every shortfall.- The import direction, in the same test: the package's sources import
nothing from
cs_image_system.base,.systemor.hashicorp_utils, and each kind module imports onlycoreand the kinds it declares. tests/test_v2_package_metadata.py holds the same line from the manifest: the package declares exactly the third-party distributions it imports and reaches no sibling. - At load, the loader checks what an entry point returned with
isinstance(..., PluginMetadataProtocol)and logs an error for anything else; the registry refuses acsis_nameregistered twice under one classification (Type Collision).
When it fails
AssertionError: <class> does not conform to <protocol>: <member>: ...fromassert_conforms: the class lacks a member or a method's parameters differ from the protocol's. Add the member or match the signature; the protocol is the specification. If the shortfall is in a core base class, the contract changed and the core did not follow: that is the test doing its job, and the fix is inbase.Plugin <name> did not return a valid PluginMetadataProtocol instancein the log at load, and the plugin's types unknown to everytype:: the entry point returned something other than aPluginMetadata(or a list of them) -- a class instead of an instance, a dict, nothing.Type Collision: <name>at load: two plugins register onecsis_nameunder one classification; rename one.BackendWithoutKind(state): aBackendRegistrationwas built without itskind; the model'sto_backend_registration()must name the type'sBackendKindProtocolobject.<Model> ... Extra inputs are not permittedat load: a YAML key the model does not declare (CSIS_MODEL_CONFIGrefuses unknown keys); the plugin's README names the fields it reads.ModuleNotFoundError: No module named 'cs_image_system.base.protocols'from a plugin written before stage 30 running on this tree: the protocols moved here, with no shim; import them fromcs_image_system.contract.core.
No failure of this package has happened in the field yet (2026-10-29).
Related
- docs/PLUGINS.md, the index of every package.
- base, the core that implements the contract.
Metadata
Release files for cs-image-system-contract 0.1.4
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 |
|---|---|---|---|---|
| cs_image_system_contract-0.1.4.tar.gz | 35.2 kB | Without provenance | Details for cs_image_system_contract-0.1.4.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| cs_image_system_contract-0.1.4-py3-none-any.whl | Python 3 | none | any | Details for cs_image_system_contract-0.1.4-py3-none-any.whl |
Total release size: 82.8 kB
Release files / cs_image_system_contract-0.1.4.tar.gz
| Download URL | cs_image_system_contract-0.1.4.tar.gz |
|---|---|
| Size | 35.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a9ce68df03b5aea8c1f7ee286c73b426c0540026ee7b3ae6dd6a46e2ac6b197f
|
|
BLAKE2b-256 checksum How to use checksums |
113cd2773c4bd9e74729dcfb3d5af4a1ed85f2f5738564c57c32b3c4ecb0a6d7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.18 {"installer":{"name":"uv","version":"0.12.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / cs_image_system_contract-0.1.4-py3-none-any.whl
| Download URL | cs_image_system_contract-0.1.4-py3-none-any.whl |
|---|---|
| Size | 47.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e66f83236747baa350500831d261913ece40029260f6ad368dbf646b21289203
|
|
BLAKE2b-256 checksum How to use checksums |
ad563fdceffaf652cf1ad0b6756e52abf61324fc135a3bf8a3a595d3285dd974
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.18 {"installer":{"name":"uv","version":"0.12.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for cs-image-system-contract 0.1.4
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.