Robotics Runtime Contracts
Canonical, machine-verifiable contracts for portable robotics executions.
This package defines the boundary shared by product repositories, runtime infrastructure, and acceptance tooling. It validates requested scenarios, observed runtimes, evidence, qualification inputs, and verdicts. It does not launch ROS 2, choose a simulator, collect telemetry, or contain product logic.
Architecture
flowchart LR
product["Product repository"] --> infra["Runtime infrastructure"]
infra --> execution["ROS 2 execution"]
execution --> harness["Acceptance harness"]
harness --> evidence["Evidence and verdict"]
contracts["Runtime contracts"] -. validates .-> product
contracts -. validates .-> infra
contracts -. validates .-> harness
The contracts are independent of a particular robot or product. They describe ROS 2 execution, security and timing concepts, including implementation-specific constraints. Provider identities are recorded as data or namespaced extensions; they do not select a schema.
Install
Python 3.12 through 3.14 is supported. Install a wheel from a tagged GitHub Release, or create a development environment:
uv sync --locked --all-groups
Release assets include build-provenance attestations. See SUPPLY_CHAIN.md.
CLI
Validate JSON or YAML using its declared schema_version:
robotics-contracts validate scenario.yaml
Resolve reviewed RFC 7396 overlays and retain their origin trace:
robotics-contracts scenario resolve base.yaml \
--overlay camera.yaml \
--overlay limits.yaml \
--output resolved.yaml \
--trace-output resolution-trace.json
Qualification subjects use the following naming pattern. Add the evidence index, retained raw artifacts, and other bindings required by the scenario to make the set complete; see qualification validation.
robotics-contracts validate-qualification \
--artifact scenario:scenario.json=scenario.json \
--artifact acceptance_run:acceptance-run.json=run.json \
--artifact runtime_manifest:runtime-manifests/main.json=runtime.json \
--artifact domain_result:results/main.json=result.json \
--artifact acceptance_aggregate:acceptance-aggregate.json=aggregate.json
Use --quiet for gates and --format json for stable machine-readable
diagnostics. describe reports a schema identifier and digest, diff emits an
RFC 7396 merge patch, and permit init --subject-digest ... creates an unsigned
physical-execution permit for an external signing workflow.
Python API
from robotics_runtime_contracts import (
schema_for_role,
schema_registry,
validate_document,
validate_role,
)
validate_document(document)
validate_role(document, "acceptance_scenario")
print(schema_for_role("runtime_manifest"))
registry = schema_registry()
Validation is offline, does not mutate inputs, and rejects non-finite numbers.
Individual document validation reports its first failure; structural and semantic
errors normally include a JSON path. The public qualification module accumulates
independent document and link failures in QualificationReport, with explicit
blocked checks when prerequisites are invalid. Link errors can have no JSON path.
The package exports worst_status() for consumers to share status-folding
rules; this does not mean every consumer already uses it.
Contract Set
Release 0.16 publishes one catalogued v1 contract set. These identifiers are
not compatible with every historical v1 document. The
machine-readable source of truth is
catalog.v1.json.
| Area | Public roles |
|---|---|
| Execution | scenario, run, observation, result, aggregate, campaign |
| Runtime | runtime manifest, model manifest, dataset manifest, robot description |
| Evidence | evidence index, recording summary, artifact receipt |
| Qualification | profile, conformance result, bundle, policy |
| Physical safety | execution permit, verification, trust policy |
| Cross-domain transport | channel, observation, clock relation, causal chain, qualification result |
Every public document uses JSON Schema Draft 2020-12, declares a
schema_version ending in .v1, rejects unknown root fields, and has an ID in
the urn:robotics-runtime-contracts:v1:* namespace. Internal schema resources
exist only to remove duplication and are not document roles.
The working catalog adds execution_trust_policy and robot_description for
the next release; these roles were not published in 0.16. See
product artifact roles for their field and
path conventions and consumer examples for
complete documents with real artifact byte digests.
Extensions
Every public role supports extension_schemas declarations and namespaced
extensions without changing the common contract. The qualification bundle
places both fields inside its in-toto predicate; other roles use the root.
Extension schemas are interpreted as Draft 2020-12. Extension keys use
reverse-domain namespaces such as
org.example.sorting; schema bytes are supplied by the caller and are never
fetched from the network.
For compatibility, seven existing v1 roles retain unpinned extensions when
extension_schemas is absent: acceptance result, dataset manifest, evidence
index, execution permit, execution verification, model artifact manifest, and
runtime manifest. Adding the field opts into strict validation, including when
the declaration list is empty. An unpinned legacy payload has no schema-integrity
guarantee. All other roles require a declaration for each payload namespace.
The declaration digest identifies the exact supplied schema bytes. The URI must
match the schema's $id, and registry keys must be those URIs. Duplicate or
unmatched namespaces fail validation. A qualification statement's validated
extensions are domain claims; its core subjects, digests and classifications
still must exactly match the locally validated artifacts.
validate_document(
scenario,
extension_schemas={
"https://schemas.example.org/sorting.v1.schema.json": schema_bytes,
},
)
Promote an extension into the common catalog only after it has reusable semantics and evidence from more than one domain.
Merge patches and semantic diff
Scenario overlays use the native, typed implementation of
RFC 7396 section 2.
Object members merge recursively, null removes a member, and arrays replace
as a whole. Inputs and outputs do not share mutable containers. Overlays apply
in the given order, followed by contract validation.
semantic_diff compares parsed values recursively without Python's scalar
coercions: booleans differ from numbers, and integer/float representations such
as 1 and 1.0 also produce a patch. Object key order and source formatting are
ignored. A round-trip check rejects targets that require introducing an object
member with value null, using diff.unrepresentable; an unchanged existing
null and nulls inside replaced arrays are representable. This distinction is
part of the diff API; RFC 7396 specifies patch application, not diff generation.
The implementation is checked against the RFC's 15 Appendix A vectors and
generated round-trip cases. It has no third-party merge-patch dependency.
JSON and YAML input
Documents are UTF-8 mappings with string keys and finite JSON values. Files
ending in .json are parsed strictly as JSON, without a YAML fallback;
.yaml/.yml select YAML. For unnamed input, standard input, and other suffixes,
a leading { or [ selects strict JSON and other input selects YAML. Pass a
YAML source_name to loads_mapping when using YAML flow syntax such as
{key: value}. Duplicate keys are rejected in both formats, including nested
objects and escaped spellings of the same JSON key. Digest-pinned extension
schemas use the same strict JSON parser.
The YAML loader uses the YAML 1.2 core scalar rules:
dates and timestamps stay strings; yes, no, on, off, 1:30, 0b10 and
1_000 stay strings; only the core true/false spellings become booleans.
010 is decimal 10; 0o10 is octal 8 and 0x10 is hexadecimal 16. 1.10 and
1e2 are numbers; quote them when they represent textual versions or IDs.
Non-finite numbers, non-string keys, non-core tags, aliases (including recursive
and merge aliases), and multiple YAML documents are rejected. << has no merge
semantics. YAML output quotes strings using the same scalar rules and emits no
aliases, so reading a written document preserves its JSON values.
Each contract document or extension schema is limited to 8 MiB of UTF-8 input,
64 node levels (root at level 1), and 100,000 nodes, counting mapping keys and
values. File and stdin reads stop at the byte limit plus one sentinel byte.
These bounds apply to document loaders, not retained raw evidence files.
Limit failures use input.limit_exceeded, duplicates use input.duplicate_key
with the member path, and aliases use input.yaml_alias. Other malformed input
uses input.parse_failed; non-finite values use input.non_finite_number.
Deterministic JSON and artifact hashes
dumps_canonical(document) -> bytes implements this project's deterministic
JSON profile. It is not RFC 8785/JCS. Existing contract integers, including
nanoseconds beyond 2**53, remain exact JSON number tokens. No schemas or wire
types change to satisfy JCS's binary64 domain. This is an explicit compatibility
choice, not a fallback from a strict JCS implementation.
The profile accepts built-in dict, list, str, int, float, bool and
None values. Object keys must be strings. It emits UTF-8 without a BOM or
trailing newline, with compact ,/: separators. Object keys sort recursively
by Unicode code point, which differs from JCS's UTF-16 ordering; array order
is preserved. Unicode is not normalized, non-ASCII characters remain UTF-8,
and JSON control/quote/backslash escaping uses the native Python JSON encoder.
Surrogate code points in Python strings are rejected. A valid JSON surrogate
escape pair parsed into a Unicode scalar is supported.
Integers have no 53-bit or 64-bit cap and are never converted to float or string
values. Booleans stay distinct from integers. Finite floats use native Python
JSON spelling: 1.0, 1e-06, and -0.0 remain those spellings, including the
negative-zero sign. This preserves the supplied float; it cannot recover
decimal precision already lost before the call. Tuples, sets, bytes, Decimal,
custom objects and subclasses of the accepted built-ins are not coerced.
The input tree uses the same 64-level and 100,000-node limits as the loaders,
counting keys and values with the root at level 1. Encoded output is limited to
8 MiB, including escaping and UTF-8 expansion. Individual oversized strings
and integers are rejected before assembling output. The interpreter's integer
decimal-conversion limit also applies and is never changed process-wide.
Shared containers are expanded within the node budget; cycles fail the depth
limit. Errors inherit ContractError: input.invalid_type,
input.invalid_unicode, input.non_finite_number, or input.limit_exceeded.
Paths identify the invalid value, the object containing a non-string or oversized
key, or $ for the total output-byte limit. Diagnostic paths also have an 8 MiB
UTF-8 budget: when a member or index would exceed it, errors in that subtree use
the nearest enclosing path that fits. Key sizes are checked before constructing
their paths, and diagnostic escaping uses bounded chunks. This does not narrow
the accepted keys or change serialized bytes. Serialization does not mutate the
input.
from robotics_runtime_contracts import dumps_canonical, file_sha256
encoded = dumps_canonical({"ns": 1785067200123456789, "ready": True})
assert encoded == b'{"ns":1785067200123456789,"ready":true}'
digest = file_sha256("existing-evidence.bin")
file_sha256(path) -> str returns the lowercase SHA-256 hex digest of the
original file bytes, read in 1 MiB chunks. It expands home paths, propagates
normal file I/O errors and does not apply the document-size limit to artifacts.
It never parses or serializes the file, even for JSON, and works on binary data.
There is no document_digest API. Existing receipts and signatures continue
to refer to original bytes. Re-serializing a separate copy can change its hash;
replacing an existing artifact requires rebuilding its dependent hash/signature
chain. Hash new files only after their exact bytes have been written.
Errors and resource paths
Expected validation and document-operation failures inherit from the public
ContractError, which remains a ValueError. Existing specialized exception
classes and their constructor signatures remain available. Every contract
error has an error_id and a json_path (None when no document location
applies). The CLI keeps its JSON diagnostic envelope
{"error": {"error_id": "...", "message": "...", "path": "..."}}; path is
omitted when unavailable. Schema diagnostics use jsonschema's best_match,
including nested anyOf/oneOf errors, so the chosen message can change. See
jsonschema's selection rules.
CLI exit codes are 0 for success, 1 for invalid input, I/O failures or internal
errors, and 2 for invalid arguments (including conflicting paths and malformed
artifact/extension options). I/O errors use input.io_error; unexpected
exceptions use internal.error without a traceback. Python I/O APIs retain
their normal OSError behavior. Library programming errors are not converted
to validation failures. --help and process interrupts retain normal behavior.
All CLI file inputs and outputs expand ~ before accessing the filesystem.
Error families include schema.validation_failed, schema.unknown,
schema.role_unknown, schema.role_mismatch, schema.reference_invalid,
semantic.validation_failed, extension.validation_failed,
merge.schema_version_changed, qualification.invalid,
qualification.unknown_domain, qualification.unknown_channel,
provider.requirements_unsatisfied, receipt.validation_failed,
clock.evidence_invalid, status.invalid, input.parse_failed,
input.non_finite_number, input.invalid_timestamp, input.invalid,
and cli.arguments_invalid. Extension references resolve offline; dangling or
non-terminating references encountered during validation are extension errors.
Timestamp comparisons share one offset-aware parser; timestamp format
validation remains the responsibility of the contract schema.
schema_dir() and schema_path() still return pathlib.Path objects. For
zip imports, importlib.resources.as_file extracts the schema directory once
and its context stays open until process exit; an atexit handler removes the
temporary directory, following the
resource context lifetime.
Callers can retain these paths within the process without
adopting a context-manager API. Paths must not be persisted for another process.
Version Policy
Known consumers include the acceptance harness and runtime infra. They use different contract generations; see the dated consumer table in COMPATIBILITY.md and the 0.15 to 0.16 migration guide. Published schema names now permit only additive changes; breaking changes require a new schema major and migration notes. The structural compatibility gate is planned, so published schema changes remain deferred. Released tags and artifacts remain immutable. See CHANGELOG.md.
HIL and real-target contracts are observation-only. A valid document is not an authorization to actuate hardware and is not proof that a device or accelerator has been qualified.
Development
uv sync --locked --all-groups
uv run pre-commit run --all-files --show-diff-on-failure
uv run pytest
uv build --no-sources
Consumer examples live in consumer-examples/.
Contributions must follow CONTRIBUTING.md, and security
reports must follow SECURITY.md.
License
Metadata
Release files for robotics-runtime-contracts 0.18.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| robotics_runtime_contracts-0.18.1.tar.gz | 167.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| robotics_runtime_contracts-0.18.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 291.7 kB
Release files / robotics_runtime_contracts-0.18.1.tar.gz
| Download URL | robotics_runtime_contracts-0.18.1.tar.gz |
|---|---|
| Size | 167.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6f54db6a588869c3835e7d9883c81aa693cd380c9d0990a468be9645254dcdd2
|
|
BLAKE2b-256 checksum How to use checksums |
da33c0df9704f192d9bdb40b36b9ac284fd11a6db43ef9b000a9eb4019ebab49
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.
Transparency logRelease files / robotics_runtime_contracts-0.18.1-py3-none-any.whl
| Download URL | robotics_runtime_contracts-0.18.1-py3-none-any.whl |
|---|---|
| Size | 123.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1bd7ef9c88668382d121533cef5bedd733a799385dec03ef016c24866608bc62
|
|
BLAKE2b-256 checksum How to use checksums |
3a5e7a1c2c8680dc27aab0fbf9725431ee1fb8a95188c8285ea465bfdc5902e8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.
Transparency log