validibot-shared 0.17.0__tar.gz → 0.18.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/PKG-INFO +1 -1
  2. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/pyproject.toml +1 -1
  3. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/tests/test_evidence_manifest.py +100 -0
  4. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/validibot_shared/__init__.py +6 -0
  5. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/validibot_shared/evidence/__init__.py +6 -0
  6. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/validibot_shared/evidence/manifest.py +197 -4
  7. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/.gitignore +0 -0
  8. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/LICENSE +0 -0
  9. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/NOTICE +0 -0
  10. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/README.md +0 -0
  11. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/tests/__init__.py +0 -0
  12. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/tests/test_artifacts.py +0 -0
  13. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/tests/test_callback_nonce_contract.py +0 -0
  14. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/tests/test_canonicalization.py +0 -0
  15. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/tests/test_energyplus_envelopes.py +0 -0
  16. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/tests/test_energyplus_models.py +0 -0
  17. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/tests/test_fmu_envelopes.py +0 -0
  18. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/tests/test_fmu_models.py +0 -0
  19. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/tests/test_package_init.py +0 -0
  20. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/tests/test_schematron_envelopes.py +0 -0
  21. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/tests/test_schematron_svrl.py +0 -0
  22. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/tests/test_shacl_envelopes.py +0 -0
  23. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/tests/test_validations_envelopes.py +0 -0
  24. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/validibot_shared/_vendor/__init__.py +0 -0
  25. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/validibot_shared/_vendor/jcs.py +0 -0
  26. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/validibot_shared/canonicalization.py +0 -0
  27. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/validibot_shared/energyplus/__init__.py +0 -0
  28. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/validibot_shared/energyplus/envelopes.py +0 -0
  29. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/validibot_shared/energyplus/models.py +0 -0
  30. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/validibot_shared/fmu/__init__.py +0 -0
  31. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/validibot_shared/fmu/envelopes.py +0 -0
  32. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/validibot_shared/fmu/models.py +0 -0
  33. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/validibot_shared/py.typed +0 -0
  34. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/validibot_shared/schematron/__init__.py +0 -0
  35. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/validibot_shared/schematron/envelopes.py +0 -0
  36. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/validibot_shared/schematron/svrl.py +0 -0
  37. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/validibot_shared/shacl/__init__.py +0 -0
  38. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/validibot_shared/shacl/envelopes.py +0 -0
  39. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/validibot_shared/validations/__init__.py +0 -0
  40. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/validibot_shared/validations/artifacts.py +0 -0
  41. {validibot_shared-0.17.0 → validibot_shared-0.18.0}/validibot_shared/validations/envelopes.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: validibot-shared
3
- Version: 0.17.0
3
+ Version: 0.18.0
4
4
  Summary: Shared library for data interchange between Validibot and validator containers
5
5
  Project-URL: Homepage, https://validibot.com
6
6
  Project-URL: Documentation, https://docs.validibot.com
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "validibot-shared"
7
- version = "0.17.0"
7
+ version = "0.18.0"
8
8
  description = "Shared library for data interchange between Validibot and validator containers"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -19,6 +19,8 @@ What this file covers
19
19
  5. Artifact lineage fields are additive, evidence-safe projections:
20
20
  hashes and stable IDs are allowed; private storage URIs are not part
21
21
  of the public schema.
22
+ 6. Attempt-bound execution evidence records canonical envelopes, verified
23
+ input identities, and explicit preprocessing relationships without URIs.
22
24
  """
23
25
 
24
26
  from __future__ import annotations
@@ -33,6 +35,9 @@ from validibot_shared.evidence import (
33
35
  EvidenceManifest,
34
36
  ManifestArtifactInputBinding,
35
37
  ManifestArtifactLineageEdge,
38
+ ManifestExecutionAttempt,
39
+ ManifestExecutionInput,
40
+ ManifestInputRelationship,
36
41
  ManifestPayloadDigests,
37
42
  ManifestProducedArtifact,
38
43
  ManifestRetentionInfo,
@@ -207,6 +212,7 @@ class TestArtifactLineageFields:
207
212
  contract_key="generated_model",
208
213
  filename="model.epjson",
209
214
  sha256="a" * 64,
215
+ storage_version="42",
210
216
  )
211
217
  binding = ManifestArtifactInputBinding(
212
218
  target_step_id=20,
@@ -217,6 +223,7 @@ class TestArtifactLineageFields:
217
223
  source_artifact_id="artifact-1",
218
224
  source_filename="model.epjson",
219
225
  source_sha256="a" * 64,
226
+ source_storage_version="42",
220
227
  producer_step_key="build_model",
221
228
  producer_contract_key="generated_model",
222
229
  resolved=True,
@@ -245,6 +252,99 @@ class TestArtifactLineageFields:
245
252
  assert not hasattr(restored.artifact_input_bindings[0], "uri")
246
253
 
247
254
 
255
+ # ──────────────────────────────────────────────────────────────────────
256
+ # Attempt-bound execution evidence — immutable files and transformations
257
+ # ──────────────────────────────────────────────────────────────────────
258
+
259
+
260
+ class TestExecutionAttemptEvidence:
261
+ """Pin the additive strict-execution evidence contract for verifiers."""
262
+
263
+ def test_execution_attempts_default_to_empty(self):
264
+ """Inline validators and older producers remain valid without attempts."""
265
+ manifest = EvidenceManifest(**_minimal_manifest_kwargs())
266
+
267
+ assert manifest.execution_attempts == []
268
+ assert manifest.schema_version == "validibot.evidence.v1"
269
+
270
+ def test_attempt_records_round_trip_without_storage_uris(self):
271
+ """Portable evidence keeps immutable identities but exposes no file URI."""
272
+ execution_input = ManifestExecutionInput(
273
+ channel="input_files",
274
+ name="generated.epjson",
275
+ role="primary-model",
276
+ port_key="primary_model",
277
+ media_type="application/json",
278
+ size_bytes=123,
279
+ sha256="b" * 64,
280
+ storage_version="sha256:" + "b" * 64,
281
+ )
282
+ relationship = ManifestInputRelationship(
283
+ source_kind="submission",
284
+ source_id="submission-1",
285
+ source_name="template.epjson",
286
+ source_size_bytes=100,
287
+ source_sha256="a" * 64,
288
+ target_channel="input_files",
289
+ target_name="generated.epjson",
290
+ target_port_key="primary_model",
291
+ target_sha256="b" * 64,
292
+ relationship="transformed",
293
+ transformation="energyplus-template-substitution.v1",
294
+ )
295
+ attempt = ManifestExecutionAttempt(
296
+ execution_attempt_id="attempt-1",
297
+ step_run_id="step-run-1",
298
+ attempt_number=1,
299
+ state="COMPLETED",
300
+ runner_type="cloud_run_job",
301
+ provider_execution_id="execution-1",
302
+ attempt_contract_version="validibot.attempt.v2",
303
+ input_envelope_sha256="c" * 64,
304
+ output_envelope_sha256="d" * 64,
305
+ backend_image_digest="registry/energyplus@sha256:" + "e" * 64,
306
+ inputs_verified=True,
307
+ input_files=[execution_input],
308
+ input_relationships=[relationship],
309
+ )
310
+
311
+ manifest = EvidenceManifest(
312
+ **_minimal_manifest_kwargs(),
313
+ execution_attempts=[attempt],
314
+ )
315
+ restored = EvidenceManifest.model_validate(manifest.model_dump(mode="json"))
316
+
317
+ assert restored == manifest
318
+ assert restored.execution_attempts[0].inputs_verified is True
319
+ assert not hasattr(restored.execution_attempts[0].input_files[0], "uri")
320
+
321
+ def test_relationship_keeps_original_and_executed_digests_distinct(self):
322
+ """Preprocessing evidence cannot imply original bytes were executed."""
323
+ relationship = ManifestInputRelationship(
324
+ source_kind="submission",
325
+ source_sha256="a" * 64,
326
+ target_channel="input_files",
327
+ target_name="generated.idf",
328
+ target_sha256="b" * 64,
329
+ relationship="transformed",
330
+ transformation="energyplus-template-substitution.v1",
331
+ )
332
+
333
+ assert relationship.source_sha256 != relationship.target_sha256
334
+ assert relationship.relationship == "transformed"
335
+
336
+ def test_execution_input_rejects_invalid_integrity_identity(self):
337
+ """Evidence cannot accept an invalid digest or negative byte length."""
338
+ with pytest.raises(ValidationError):
339
+ ManifestExecutionInput(
340
+ channel="input_files",
341
+ name="model.idf",
342
+ size_bytes=-1,
343
+ sha256="not-a-sha256",
344
+ storage_version="generation-1",
345
+ )
346
+
347
+
248
348
  # ──────────────────────────────────────────────────────────────────────
249
349
  # WorkflowContractSnapshot — constants, signal-mapping defs, definition hash
250
350
  # ──────────────────────────────────────────────────────────────────────
@@ -19,6 +19,9 @@ from validibot_shared.evidence import (
19
19
  ContractConstant,
20
20
  ContractSignalMapping,
21
21
  EvidenceManifest,
22
+ ManifestExecutionAttempt,
23
+ ManifestExecutionInput,
24
+ ManifestInputRelationship,
22
25
  ManifestPayloadDigests,
23
26
  ManifestRetentionInfo,
24
27
  StepValidatorRecord,
@@ -47,6 +50,9 @@ __all__ = [
47
50
  "FMUProbeResult",
48
51
  "FMUVariableMeta",
49
52
  "ManifestPayloadDigests",
53
+ "ManifestExecutionAttempt",
54
+ "ManifestExecutionInput",
55
+ "ManifestInputRelationship",
50
56
  "ManifestRetentionInfo",
51
57
  "SHACLInputEnvelope",
52
58
  "SHACLInputs",
@@ -14,6 +14,9 @@ from validibot_shared.evidence.manifest import (
14
14
  EvidenceManifest,
15
15
  ManifestArtifactInputBinding,
16
16
  ManifestArtifactLineageEdge,
17
+ ManifestExecutionAttempt,
18
+ ManifestExecutionInput,
19
+ ManifestInputRelationship,
17
20
  ManifestPayloadDigests,
18
21
  ManifestProducedArtifact,
19
22
  ManifestRetentionInfo,
@@ -28,6 +31,9 @@ __all__ = [
28
31
  "EvidenceManifest",
29
32
  "ManifestArtifactInputBinding",
30
33
  "ManifestArtifactLineageEdge",
34
+ "ManifestExecutionAttempt",
35
+ "ManifestExecutionInput",
36
+ "ManifestInputRelationship",
31
37
  "ManifestPayloadDigests",
32
38
  "ManifestProducedArtifact",
33
39
  "ManifestRetentionInfo",
@@ -252,10 +252,11 @@ class ManifestPayloadDigests(BaseModel):
252
252
  input_sha256: str | None = Field(
253
253
  default=None,
254
254
  description=(
255
- "SHA-256 hex of the submission's primary file bytes at run "
256
- "time. Always present in Session B even when "
257
- "DO_NOT_STORE purges the bytes themselves, the hash is "
258
- "the immutable proof the run consumed input matching it."
255
+ "SHA-256 hex of the submission's primary file bytes accepted by "
256
+ "the producer. Always present in Session B, even when "
257
+ "DO_NOT_STORE purges the bytes themselves. This does not by "
258
+ "itself claim the same bytes crossed the execution boundary; "
259
+ "execution_attempts records that identity and any transformation."
259
260
  ),
260
261
  )
261
262
  output_envelope_sha256: str | None = Field(
@@ -309,6 +310,13 @@ class ManifestProducedArtifact(BaseModel):
309
310
  data_format: str = Field(default="", description="Domain data format.")
310
311
  size_bytes: int | None = Field(default=None, description="Artifact byte size.")
311
312
  sha256: str = Field(default="", description="SHA-256 of artifact bytes.")
313
+ storage_version: str = Field(
314
+ default="",
315
+ description=(
316
+ "Provider-specific immutable storage version verified for the "
317
+ "artifact. Empty for producers predating strict artifact identity."
318
+ ),
319
+ )
312
320
  manifest_sha256: str = Field(
313
321
  default="",
314
322
  description="SHA-256 of the validator output manifest, if present.",
@@ -390,6 +398,13 @@ class ManifestArtifactInputBinding(BaseModel):
390
398
  )
391
399
  source_size_bytes: int | None = Field(default=None)
392
400
  source_sha256: str = Field(default="", description="Resolved source SHA-256.")
401
+ source_storage_version: str = Field(
402
+ default="",
403
+ description=(
404
+ "Immutable storage version used for this binding. Empty for "
405
+ "unresolved inputs or producers predating strict file identity."
406
+ ),
407
+ )
393
408
  producer_step_key: str = Field(
394
409
  default="",
395
410
  description="Producer step key for upstream_artifact bindings.",
@@ -421,6 +436,173 @@ class ManifestArtifactLineageEdge(BaseModel):
421
436
  target_step_run_id: str = Field(default="", description="Consumer step-run id.")
422
437
 
423
438
 
439
+ class ManifestExecutionInput(BaseModel):
440
+ """URI-free identity of one file committed to an execution envelope.
441
+
442
+ The trusted producer copies these fields from the immutable input envelope
443
+ bound to an execution attempt. The strict runtime contract verifies the
444
+ exact size, digest, and storage version while streaming before domain
445
+ processing. ``ManifestExecutionAttempt.inputs_verified`` tells consumers
446
+ whether trusted completion confirmed that verification boundary was crossed.
447
+ """
448
+
449
+ model_config = ConfigDict(frozen=True, extra="forbid")
450
+
451
+ channel: Literal["input_files", "resource_files"] = Field(
452
+ description="Input-envelope collection that carried this file.",
453
+ )
454
+ name: str = Field(description="Safe logical leaf name from the input envelope.")
455
+ role: str = Field(
456
+ default="",
457
+ description="Validator-facing role for an input file.",
458
+ )
459
+ resource_type: str = Field(
460
+ default="",
461
+ description="Validator-facing resource type for a resource file.",
462
+ )
463
+ port_key: str = Field(
464
+ default="",
465
+ description="Stable Validibot file-port key, when declared.",
466
+ )
467
+ resource_id: str = Field(
468
+ default="",
469
+ description="Producer-side resource identifier, for resource files.",
470
+ )
471
+ media_type: str = Field(
472
+ default="",
473
+ description="Declared media type, when the envelope supplies one.",
474
+ )
475
+ size_bytes: int = Field(
476
+ ge=0,
477
+ description="Exact byte size enforced by the runtime's streaming read.",
478
+ )
479
+ sha256: str = Field(
480
+ pattern=r"^[0-9a-f]{64}$",
481
+ description="Lowercase SHA-256 of the exact execution-boundary bytes.",
482
+ )
483
+ storage_version: str = Field(
484
+ min_length=1,
485
+ max_length=512,
486
+ description="Provider-specific immutable object/version identity.",
487
+ )
488
+
489
+
490
+ class ManifestInputRelationship(BaseModel):
491
+ """Relationship between source bytes and an execution-envelope file.
492
+
493
+ This makes preprocessing explicit. For example, an EnergyPlus template can
494
+ be recorded as the source while the generated IDF/epJSON file is the target,
495
+ preventing evidence from implying the original template bytes were executed.
496
+ """
497
+
498
+ model_config = ConfigDict(frozen=True, extra="forbid")
499
+
500
+ source_kind: str = Field(
501
+ description=(
502
+ "Source category, such as submission, workflow_resource, "
503
+ "upstream_artifact, or generated."
504
+ ),
505
+ )
506
+ source_id: str = Field(
507
+ default="",
508
+ description="Stable producer-side source identifier, when available.",
509
+ )
510
+ source_name: str = Field(
511
+ default="",
512
+ description="Evidence-safe source filename or logical name.",
513
+ )
514
+ source_size_bytes: int | None = Field(
515
+ default=None,
516
+ ge=0,
517
+ description="Exact source size, when durably known.",
518
+ )
519
+ source_sha256: str = Field(
520
+ pattern=r"^[0-9a-f]{64}$",
521
+ description="Lowercase SHA-256 of the source bytes.",
522
+ )
523
+ target_channel: Literal["input_files", "resource_files"] = Field(
524
+ description="Input-envelope collection containing the target file.",
525
+ )
526
+ target_name: str = Field(description="Target file's safe logical name.")
527
+ target_port_key: str = Field(
528
+ default="",
529
+ description="Stable file-port key that disambiguates the target.",
530
+ )
531
+ target_sha256: str = Field(
532
+ pattern=r"^[0-9a-f]{64}$",
533
+ description="Lowercase SHA-256 of the execution-boundary target bytes.",
534
+ )
535
+ relationship: str = Field(
536
+ description="Explicit relationship, such as identical or transformed.",
537
+ )
538
+ transformation: str = Field(
539
+ default="",
540
+ description=(
541
+ "Stable transformation identifier when relationship is transformed."
542
+ ),
543
+ )
544
+
545
+
546
+ class ManifestExecutionAttempt(BaseModel):
547
+ """Attempt-bound execution identity projected without storage URIs.
548
+
549
+ Only attempts with a committed canonical input-envelope digest belong in
550
+ this list. Failed retries may therefore appear, but an attempt must not set
551
+ ``inputs_verified`` merely because files were declared. The producer sets it
552
+ only after trusted completion confirms the strict backend crossed the input
553
+ verification boundary.
554
+ """
555
+
556
+ model_config = ConfigDict(frozen=True, extra="forbid")
557
+
558
+ execution_attempt_id: str = Field(description="ExecutionAttempt UUID.")
559
+ step_run_id: str = Field(description="Step-run identity executed by the attempt.")
560
+ attempt_number: int = Field(
561
+ ge=1,
562
+ description="Monotonic attempt number within the step run.",
563
+ )
564
+ state: str = Field(description="Durable terminal attempt state at evidence time.")
565
+ runner_type: str = Field(description="Execution provider/runner type.")
566
+ provider_execution_id: str = Field(
567
+ default="",
568
+ description="Provider-assigned execution identity, when available.",
569
+ )
570
+ attempt_contract_version: str = Field(
571
+ description="Strict attempt contract version used by the input envelope.",
572
+ )
573
+ input_envelope_sha256: str = Field(
574
+ pattern=r"^[0-9a-f]{64}$",
575
+ description="Canonical SHA-256 committed before provider execution.",
576
+ )
577
+ output_envelope_sha256: str | None = Field(
578
+ default=None,
579
+ pattern=r"^[0-9a-f]{64}$",
580
+ description=(
581
+ "Canonical trusted output-envelope SHA-256, when produced and "
582
+ "permitted by retention policy."
583
+ ),
584
+ )
585
+ backend_image_digest: str = Field(
586
+ default="",
587
+ description="Resolved digest of the backend image used by this attempt.",
588
+ )
589
+ inputs_verified: bool = Field(
590
+ default=False,
591
+ description=(
592
+ "Whether trusted completion confirms strict streaming verification "
593
+ "completed before domain processing."
594
+ ),
595
+ )
596
+ input_files: list[ManifestExecutionInput] = Field(
597
+ default_factory=list,
598
+ description="URI-free file identities copied from the committed envelope.",
599
+ )
600
+ input_relationships: list[ManifestInputRelationship] = Field(
601
+ default_factory=list,
602
+ description="Source-to-execution relationships, including preprocessing.",
603
+ )
604
+
605
+
424
606
  class EvidenceManifest(BaseModel):
425
607
  """Top-level evidence manifest for a single validation run.
426
608
 
@@ -498,6 +680,14 @@ class EvidenceManifest(BaseModel):
498
680
  "artifact_input_bindings but do not create producer-step edges."
499
681
  ),
500
682
  )
683
+ execution_attempts: list[ManifestExecutionAttempt] = Field(
684
+ default_factory=list,
685
+ description=(
686
+ "Attempt-bound canonical envelope, provider, backend image, verified "
687
+ "file, and preprocessing evidence. Empty for inline validators or "
688
+ "producers predating strict execution evidence."
689
+ ),
690
+ )
501
691
  # The auth channel that initiated the run. Pinning it in the
502
692
  # manifest lets verifiers answer "what surface produced this
503
693
  # run?" without consulting the producer database (the run row
@@ -525,6 +715,9 @@ __all__ = [
525
715
  "EvidenceManifest",
526
716
  "ManifestArtifactInputBinding",
527
717
  "ManifestArtifactLineageEdge",
718
+ "ManifestExecutionAttempt",
719
+ "ManifestExecutionInput",
720
+ "ManifestInputRelationship",
528
721
  "ManifestPayloadDigests",
529
722
  "ManifestProducedArtifact",
530
723
  "ManifestRetentionInfo",