rexecop 0.2.2a0__py3-none-any.whl

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 (90) hide show
  1. rexecop/__init__.py +3 -0
  2. rexecop/__main__.py +4 -0
  3. rexecop/adapters/__init__.py +1 -0
  4. rexecop/adapters/govengine_port/__init__.py +19 -0
  5. rexecop/adapters/govengine_port/adapter.py +19 -0
  6. rexecop/adapters/govengine_port/client.py +161 -0
  7. rexecop/adapters/govengine_port/contracts.py +88 -0
  8. rexecop/adapters/govengine_port/static_adapter.py +39 -0
  9. rexecop/adapters/sclite_port/__init__.py +26 -0
  10. rexecop/adapters/sclite_port/contracts.py +149 -0
  11. rexecop/adapters/sclite_port/emitter.py +408 -0
  12. rexecop/adapters/sclite_port/execution_receipt_metrics.py +76 -0
  13. rexecop/adapters/sclite_port/fixture_bundle.py +49 -0
  14. rexecop/adapters/sclite_port/full_bundle.py +430 -0
  15. rexecop/adapters/sclite_port/govengine_policy_bridge.py +31 -0
  16. rexecop/adapters/sclite_port/placeholder_emitter.py +83 -0
  17. rexecop/adapters/sclite_port/target_host.py +32 -0
  18. rexecop/cli.py +376 -0
  19. rexecop/connectors/__init__.py +17 -0
  20. rexecop/connectors/base.py +35 -0
  21. rexecop/connectors/capability.py +35 -0
  22. rexecop/connectors/command_shape.py +30 -0
  23. rexecop/connectors/composite_runtime.py +118 -0
  24. rexecop/connectors/errors.py +14 -0
  25. rexecop/connectors/fixture_loader.py +63 -0
  26. rexecop/connectors/http_api.py +374 -0
  27. rexecop/connectors/http_support.py +83 -0
  28. rexecop/connectors/local_shell.py +126 -0
  29. rexecop/connectors/mock_runtime.py +60 -0
  30. rexecop/connectors/mutating.py +13 -0
  31. rexecop/connectors/runtime.py +16 -0
  32. rexecop/connectors/ssh_readonly.py +162 -0
  33. rexecop/environment/__init__.py +4 -0
  34. rexecop/environment/loader.py +20 -0
  35. rexecop/environment/model.py +41 -0
  36. rexecop/environment/sanitize.py +57 -0
  37. rexecop/errors.py +10 -0
  38. rexecop/escalation/__init__.py +5 -0
  39. rexecop/escalation/package.py +31 -0
  40. rexecop/evidence/__init__.py +4 -0
  41. rexecop/evidence/event.py +22 -0
  42. rexecop/evidence/manager.py +47 -0
  43. rexecop/evidence/redaction.py +26 -0
  44. rexecop/examples/__init__.py +1 -0
  45. rexecop/examples/bootstrap_receipt.py +61 -0
  46. rexecop/execution/__init__.py +6 -0
  47. rexecop/execution/backend.py +29 -0
  48. rexecop/execution/executor.py +114 -0
  49. rexecop/execution/internal_registry.py +59 -0
  50. rexecop/operation/__init__.py +12 -0
  51. rexecop/operation/controller.py +554 -0
  52. rexecop/operation/model.py +111 -0
  53. rexecop/operation/plan.py +62 -0
  54. rexecop/operation/state.py +72 -0
  55. rexecop/orchestration/__init__.py +5 -0
  56. rexecop/orchestration/orchestrator.py +589 -0
  57. rexecop/profile/__init__.py +11 -0
  58. rexecop/profile/contract.py +42 -0
  59. rexecop/profile/loader.py +76 -0
  60. rexecop/profile/resolver.py +69 -0
  61. rexecop/profile/validation_rules.py +28 -0
  62. rexecop/runtime_ops/__init__.py +19 -0
  63. rexecop/runtime_ops/coordinator.py +108 -0
  64. rexecop/runtime_ops/maintenance.py +38 -0
  65. rexecop/runtime_ops/monitor.py +49 -0
  66. rexecop/runtime_ops/queue.py +76 -0
  67. rexecop/runtime_ops/rollback.py +71 -0
  68. rexecop/runtime_ops/target_lock.py +98 -0
  69. rexecop/runtime_ops/worker.py +141 -0
  70. rexecop/secrets/__init__.py +17 -0
  71. rexecop/secrets/port.py +7 -0
  72. rexecop/secrets/resolver.py +66 -0
  73. rexecop/storage/__init__.py +13 -0
  74. rexecop/storage/factory.py +27 -0
  75. rexecop/storage/file_store.py +106 -0
  76. rexecop/storage/memory_store.py +75 -0
  77. rexecop/storage/port.py +43 -0
  78. rexecop/storage/sqlite_store.py +158 -0
  79. rexecop/types.py +10 -0
  80. rexecop/validation/__init__.py +5 -0
  81. rexecop/validation/validator.py +130 -0
  82. rexecop/workflow/__init__.py +4 -0
  83. rexecop/workflow/loader.py +26 -0
  84. rexecop/workflow/model.py +93 -0
  85. rexecop/workflow/runner.py +165 -0
  86. rexecop-0.2.2a0.dist-info/METADATA +231 -0
  87. rexecop-0.2.2a0.dist-info/RECORD +90 -0
  88. rexecop-0.2.2a0.dist-info/WHEEL +4 -0
  89. rexecop-0.2.2a0.dist-info/entry_points.txt +2 -0
  90. rexecop-0.2.2a0.dist-info/licenses/LICENSE +21 -0
rexecop/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """RExecOp — Regulated Execution Operations control-plane."""
2
+
3
+ __version__ = "0.2.2a0"
rexecop/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from rexecop.cli import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
@@ -0,0 +1 @@
1
+ """External system adapters (GovEngine, SCLite)."""
@@ -0,0 +1,19 @@
1
+ from rexecop.adapters.govengine_port.adapter import default_govengine_adapter
2
+ from rexecop.adapters.govengine_port.client import GovEngineClient
3
+ from rexecop.adapters.govengine_port.contracts import (
4
+ GovEngineAdapter,
5
+ GovEngineDecision,
6
+ GovEngineDecisionType,
7
+ GovEngineRequest,
8
+ )
9
+ from rexecop.adapters.govengine_port.static_adapter import StaticGovEngineAdapter
10
+
11
+ __all__ = [
12
+ "GovEngineClient",
13
+ "GovEngineAdapter",
14
+ "GovEngineDecision",
15
+ "GovEngineDecisionType",
16
+ "GovEngineRequest",
17
+ "StaticGovEngineAdapter",
18
+ "default_govengine_adapter",
19
+ ]
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ from rexecop.adapters.govengine_port.client import GovEngineClient
4
+ from rexecop.adapters.govengine_port.contracts import GovEngineAdapter, GovEngineDecisionType
5
+ from rexecop.adapters.govengine_port.static_adapter import StaticGovEngineAdapter
6
+
7
+
8
+ def default_govengine_adapter() -> GovEngineAdapter:
9
+ """Fail-closed real GovEngine client for local development."""
10
+ return GovEngineClient()
11
+
12
+
13
+ def static_govengine_adapter(
14
+ decision_type: GovEngineDecisionType,
15
+ *,
16
+ summary: str = "",
17
+ ) -> GovEngineAdapter:
18
+ """Explicit bootstrap/test adapter."""
19
+ return StaticGovEngineAdapter(decision_type, summary=summary)
@@ -0,0 +1,161 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from govengine import compose_runtime_admission_result, runtime_admission_public_summary
6
+ from govengine.admission import RuntimeAdmissionResult
7
+
8
+ from rexecop.adapters.govengine_port.contracts import (
9
+ GovEngineDecision,
10
+ GovEngineDecisionType,
11
+ GovEngineRequest,
12
+ is_mutating_mode,
13
+ )
14
+
15
+
16
+ def map_runtime_admission_to_decision(result: RuntimeAdmissionResult) -> GovEngineDecision:
17
+ if result.allowed:
18
+ return GovEngineDecision(
19
+ decision_type=GovEngineDecisionType.ALLOWED,
20
+ summary=result.reason_code,
21
+ details={
22
+ "admission_id": result.admission_id,
23
+ "status": result.status,
24
+ "public_summary": runtime_admission_public_summary(result),
25
+ },
26
+ )
27
+
28
+ actions = set(result.required_next_actions)
29
+ blockers = set(result.blockers)
30
+
31
+ if result.status == "needs_review" or actions.intersection(
32
+ {"approve_execution_ticket", "obtain_policy_decision"}
33
+ ):
34
+ return GovEngineDecision(
35
+ decision_type=GovEngineDecisionType.APPROVAL_REQUIRED,
36
+ summary=result.reason_code,
37
+ details={
38
+ "admission_id": result.admission_id,
39
+ "status": result.status,
40
+ "blockers": list(blockers),
41
+ "required_next_actions": list(actions),
42
+ },
43
+ )
44
+
45
+ if result.status == "dry_run_only" or "live_backend_disabled" in blockers:
46
+ return GovEngineDecision(
47
+ decision_type=GovEngineDecisionType.READ_ONLY_ONLY,
48
+ summary=result.reason_code,
49
+ details={"admission_id": result.admission_id, "status": result.status},
50
+ )
51
+
52
+ return GovEngineDecision(
53
+ decision_type=GovEngineDecisionType.BLOCKED,
54
+ summary=result.reason_code,
55
+ details={
56
+ "admission_id": result.admission_id,
57
+ "status": result.status,
58
+ "blockers": list(blockers),
59
+ "required_next_actions": list(actions),
60
+ },
61
+ )
62
+
63
+
64
+ def build_compose_inputs(request: GovEngineRequest) -> dict[str, Any]:
65
+ preview = dict(request.preview)
66
+ override = preview.get("admission_compose")
67
+ if isinstance(override, dict):
68
+ return dict(override)
69
+
70
+ policy_decision = preview.get("policy_decision")
71
+ if policy_decision is None:
72
+ return {
73
+ "admission_id": f"adm-{request.operation_id}",
74
+ "subject_ref": f"rexecop:{request.operation_id}",
75
+ "prepared_execution_contract": {
76
+ "status": "prepared",
77
+ "digest": f"sha256:{request.operation_id}",
78
+ },
79
+ "policy_decision": None,
80
+ "execution_ticket": None,
81
+ "trust_decision": None,
82
+ "runner_profile": {
83
+ "name": "rexecop",
84
+ "allowed": False,
85
+ "live_backend_enabled": False,
86
+ },
87
+ "receipt_obligation": {"required": True, "binds": ["admission", "ticket"]},
88
+ "metadata": {"source": "rexecop", "phase": "2b"},
89
+ }
90
+
91
+ runner_profile = preview.get(
92
+ "runner_profile",
93
+ {
94
+ "name": "rexecop",
95
+ "allowed": True,
96
+ "live_backend_enabled": is_mutating_mode(request.mode),
97
+ },
98
+ )
99
+
100
+ return {
101
+ "admission_id": f"adm-{request.operation_id}",
102
+ "subject_ref": f"rexecop:{request.operation_id}",
103
+ "prepared_execution_contract": preview.get(
104
+ "prepared_execution_contract",
105
+ {"status": "prepared", "digest": f"sha256:{request.operation_id}"},
106
+ ),
107
+ "policy_decision": policy_decision,
108
+ "execution_ticket": preview.get("execution_ticket"),
109
+ "trust_decision": preview.get("trust_decision"),
110
+ "runner_profile": runner_profile,
111
+ "receipt_obligation": preview.get(
112
+ "receipt_obligation",
113
+ {"required": True, "binds": ["admission", "ticket"]},
114
+ ),
115
+ "artifact_refs": preview.get("artifact_refs"),
116
+ "metadata": {"source": "rexecop", "operation_id": request.operation_id},
117
+ }
118
+
119
+
120
+ class GovEngineClient:
121
+ """Real GovEngine adapter using runtime admission composition."""
122
+
123
+ def evaluate(self, request: GovEngineRequest) -> GovEngineDecision:
124
+ inputs = build_compose_inputs(request)
125
+ try:
126
+ result = compose_runtime_admission_result(
127
+ **inputs,
128
+ live=is_mutating_mode(request.mode),
129
+ )
130
+ except Exception as exc:
131
+ return GovEngineDecision(
132
+ decision_type=GovEngineDecisionType.ERROR,
133
+ summary=str(exc),
134
+ details={"adapter": "govengine_client"},
135
+ )
136
+ return map_runtime_admission_to_decision(result)
137
+
138
+
139
+ def build_runner_request_preview(
140
+ plan_steps: list[dict[str, Any]],
141
+ *,
142
+ operation_id: str,
143
+ ) -> dict[str, Any]:
144
+ """Shape helper for future runner execution (Phase 4+)."""
145
+ from govengine.execution.runner_protocol import GovRunnerRequest, GovRunnerStep
146
+
147
+ steps = tuple(
148
+ GovRunnerStep(
149
+ index=index,
150
+ tool=str(step.get("connector") or step.get("type") or "internal"),
151
+ args=(str(step.get("action") or ""),),
152
+ )
153
+ for index, step in enumerate(plan_steps)
154
+ )
155
+ request = GovRunnerRequest(
156
+ request_id=f"req-{operation_id}",
157
+ source="rexecop",
158
+ steps=steps,
159
+ dry_run=True,
160
+ )
161
+ return request.as_dict()
@@ -0,0 +1,88 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import StrEnum
5
+ from typing import Any, Protocol
6
+
7
+
8
+ class GovEngineDecisionType(StrEnum):
9
+ ALLOWED = "allowed"
10
+ BLOCKED = "blocked"
11
+ APPROVAL_REQUIRED = "approval_required"
12
+ MAINTENANCE_WINDOW_REQUIRED = "maintenance_window_required"
13
+ BACKUP_REQUIRED = "backup_required"
14
+ READ_ONLY_ONLY = "read_only_only"
15
+ HUMAN_REQUIRED = "human_required"
16
+ UNSUPPORTED = "unsupported"
17
+ ERROR = "error"
18
+
19
+
20
+ BLOCKING_DECISIONS = frozenset(
21
+ {
22
+ GovEngineDecisionType.BLOCKED,
23
+ GovEngineDecisionType.READ_ONLY_ONLY,
24
+ GovEngineDecisionType.HUMAN_REQUIRED,
25
+ GovEngineDecisionType.UNSUPPORTED,
26
+ GovEngineDecisionType.ERROR,
27
+ }
28
+ )
29
+
30
+ WAITING_DECISIONS = frozenset(
31
+ {
32
+ GovEngineDecisionType.APPROVAL_REQUIRED,
33
+ GovEngineDecisionType.MAINTENANCE_WINDOW_REQUIRED,
34
+ GovEngineDecisionType.BACKUP_REQUIRED,
35
+ }
36
+ )
37
+
38
+ MUTATING_MODES = frozenset({"apply", "recovery"})
39
+
40
+
41
+ def is_mutating_mode(mode: str) -> bool:
42
+ return mode in MUTATING_MODES
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class GovEngineRequest:
47
+ operation_id: str
48
+ profile: str
49
+ environment: str
50
+ intent: str
51
+ target: str
52
+ mode: str
53
+ risk: str
54
+ preview: dict[str, Any] = field(default_factory=dict)
55
+
56
+ def as_dict(self) -> dict[str, Any]:
57
+ return {
58
+ "operation_id": self.operation_id,
59
+ "profile": self.profile,
60
+ "environment": self.environment,
61
+ "intent": self.intent,
62
+ "target": self.target,
63
+ "mode": self.mode,
64
+ "risk": self.risk,
65
+ "preview": dict(self.preview),
66
+ }
67
+
68
+
69
+ @dataclass(frozen=True)
70
+ class GovEngineDecision:
71
+ decision_type: GovEngineDecisionType
72
+ summary: str
73
+ details: dict[str, Any] = field(default_factory=dict)
74
+
75
+ def as_dict(self) -> dict[str, Any]:
76
+ return {
77
+ "decision_type": self.decision_type.value,
78
+ "summary": self.summary,
79
+ "details": dict(self.details),
80
+ }
81
+
82
+ @property
83
+ def allows_mutating_execution(self) -> bool:
84
+ return self.decision_type == GovEngineDecisionType.ALLOWED
85
+
86
+
87
+ class GovEngineAdapter(Protocol):
88
+ def evaluate(self, request: GovEngineRequest) -> GovEngineDecision: ...
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ from rexecop.adapters.govengine_port.contracts import (
4
+ GovEngineDecision,
5
+ GovEngineDecisionType,
6
+ GovEngineRequest,
7
+ )
8
+
9
+ STATIC_ADAPTER_NOTICE = (
10
+ "StaticGovEngineAdapter is bootstrap/test only and is not production governance."
11
+ )
12
+
13
+
14
+ class StaticGovEngineAdapter:
15
+ """Bootstrap/test governance adapter. Not a policy engine."""
16
+
17
+ def __init__(
18
+ self,
19
+ decision_type: GovEngineDecisionType,
20
+ *,
21
+ summary: str = "",
22
+ details: dict[str, object] | None = None,
23
+ ) -> None:
24
+ self.decision_type = decision_type
25
+ self.summary = summary or f"static decision: {decision_type.value}"
26
+ self.details = dict(details or {})
27
+ self.bootstrap_only = True
28
+
29
+ def evaluate(self, request: GovEngineRequest) -> GovEngineDecision:
30
+ return GovEngineDecision(
31
+ decision_type=self.decision_type,
32
+ summary=self.summary,
33
+ details={
34
+ **self.details,
35
+ "adapter": "static",
36
+ "bootstrap_only": True,
37
+ "operation_id": request.operation_id,
38
+ },
39
+ )
@@ -0,0 +1,26 @@
1
+ from rexecop.adapters.sclite_port.contracts import (
2
+ ARTIFACT_SLOTS,
3
+ EVENT_SCLITE_MAPPING,
4
+ PLACEHOLDER_EMITTER_NOTICE,
5
+ RECEIPT_EXPORT_AUTHORITY,
6
+ SCLITE_SCHEMA_REFS,
7
+ SCLiteArtifactDescriptor,
8
+ SCLiteEmitter,
9
+ SCLiteReceiptExport,
10
+ )
11
+ from rexecop.adapters.sclite_port.emitter import SCLiteArtifactEmitter
12
+ from rexecop.adapters.sclite_port.placeholder_emitter import PlaceholderSCLiteEmitter
13
+
14
+ __all__ = [
15
+ "ARTIFACT_SLOTS",
16
+ "EVENT_SCLITE_MAPPING",
17
+ "PLACEHOLDER_EMITTER_NOTICE",
18
+ "RECEIPT_EXPORT_AUTHORITY",
19
+ "SCLITE_ARTIFACT_AUTHORITY",
20
+ "SCLITE_SCHEMA_REFS",
21
+ "SCLiteArtifactDescriptor",
22
+ "SCLiteArtifactEmitter",
23
+ "SCLiteEmitter",
24
+ "SCLiteReceiptExport",
25
+ "PlaceholderSCLiteEmitter",
26
+ ]
@@ -0,0 +1,149 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any, Protocol
5
+
6
+ PLACEHOLDER_EMITTER_NOTICE = (
7
+ "DEPRECATED: Placeholder SCLite emitter is bootstrap/offline only. "
8
+ "Use SCLiteArtifactEmitter for real emission (Phase 3B+). "
9
+ "Receipt exports under .rexecop/receipts/ are non-authoritative summaries."
10
+ )
11
+
12
+ SCLITE_ARTIFACT_AUTHORITY = "sclite_artifact"
13
+
14
+ RECEIPT_EXPORT_AUTHORITY = "non_authoritative_export"
15
+
16
+ SCLITE_SCHEMA_REFS: dict[str, str] = {
17
+ "intent_contract": "schemas/intent_contract.v0.2.schema.json",
18
+ "policy_decision": "schemas/policy_decision.v0.2.schema.json",
19
+ "execution_contract": "schemas/execution_contract.v0.2.schema.json",
20
+ "execution_ticket": "schemas/execution_ticket.v0.3.schema.json",
21
+ "execution_receipt": "schemas/execution_receipt.v0.2.schema.json",
22
+ "evidence_contract": "schemas/evidence_contract.v0.2.schema.json",
23
+ }
24
+
25
+ ARTIFACT_SLOTS = (
26
+ "intent_contract",
27
+ "policy_decision",
28
+ "execution_contract",
29
+ "execution_ticket",
30
+ "execution_receipt",
31
+ "evidence_contract",
32
+ )
33
+
34
+ EVENT_SCLITE_MAPPING: dict[str, dict[str, str]] = {
35
+ "operation_created": {
36
+ "future_artifact": "intent_contract",
37
+ "sclite_schema_ref": SCLITE_SCHEMA_REFS["intent_contract"],
38
+ },
39
+ "operation_triggered": {
40
+ "future_artifact": "intent_contract",
41
+ "sclite_schema_ref": SCLITE_SCHEMA_REFS["intent_contract"],
42
+ },
43
+ "plan_generated": {
44
+ "future_artifact": "execution_contract",
45
+ "sclite_schema_ref": SCLITE_SCHEMA_REFS["execution_contract"],
46
+ },
47
+ "govengine_decision_requested": {
48
+ "future_artifact": "policy_decision",
49
+ "sclite_schema_ref": SCLITE_SCHEMA_REFS["policy_decision"],
50
+ },
51
+ "govengine_decision_received": {
52
+ "future_artifact": "policy_decision",
53
+ "sclite_schema_ref": SCLITE_SCHEMA_REFS["policy_decision"],
54
+ },
55
+ "approval_received": {
56
+ "future_artifact": "execution_ticket",
57
+ "sclite_schema_ref": SCLITE_SCHEMA_REFS["execution_ticket"],
58
+ },
59
+ "state_transition": {
60
+ "future_artifact": "execution_receipt",
61
+ "sclite_schema_ref": SCLITE_SCHEMA_REFS["execution_receipt"],
62
+ },
63
+ "step_started": {
64
+ "future_artifact": "execution_receipt",
65
+ "sclite_schema_ref": SCLITE_SCHEMA_REFS["execution_receipt"],
66
+ },
67
+ "step_completed": {
68
+ "future_artifact": "execution_receipt",
69
+ "sclite_schema_ref": SCLITE_SCHEMA_REFS["execution_receipt"],
70
+ },
71
+ "step_failed": {
72
+ "future_artifact": "execution_receipt",
73
+ "sclite_schema_ref": SCLITE_SCHEMA_REFS["execution_receipt"],
74
+ },
75
+ "validation_started": {
76
+ "future_artifact": "evidence_contract",
77
+ "sclite_schema_ref": SCLITE_SCHEMA_REFS["evidence_contract"],
78
+ },
79
+ "validation_completed": {
80
+ "future_artifact": "evidence_contract",
81
+ "sclite_schema_ref": SCLITE_SCHEMA_REFS["evidence_contract"],
82
+ },
83
+ "receipt_generated": {
84
+ "future_artifact": "execution_receipt",
85
+ "sclite_schema_ref": SCLITE_SCHEMA_REFS["execution_receipt"],
86
+ },
87
+ "operation_completed": {
88
+ "future_artifact": "execution_receipt",
89
+ "sclite_schema_ref": SCLITE_SCHEMA_REFS["execution_receipt"],
90
+ },
91
+ "operation_failed": {
92
+ "future_artifact": "execution_receipt",
93
+ "sclite_schema_ref": SCLITE_SCHEMA_REFS["execution_receipt"],
94
+ },
95
+ "operation_escalated": {
96
+ "future_artifact": "evidence_contract",
97
+ "sclite_schema_ref": SCLITE_SCHEMA_REFS["evidence_contract"],
98
+ },
99
+ }
100
+
101
+
102
+ @dataclass(frozen=True)
103
+ class SCLiteArtifactDescriptor:
104
+ artifact_role: str
105
+ sclite_schema_ref: str
106
+ descriptor_path: str | None = None
107
+ digest: str | None = None
108
+ status: str = "placeholder"
109
+
110
+ def as_dict(self) -> dict[str, Any]:
111
+ return {
112
+ "artifact_role": self.artifact_role,
113
+ "sclite_schema_ref": self.sclite_schema_ref,
114
+ "descriptor_path": self.descriptor_path,
115
+ "digest": self.digest,
116
+ "status": self.status,
117
+ }
118
+
119
+
120
+ @dataclass
121
+ class SCLiteReceiptExport:
122
+ operation_id: str
123
+ authority: str = RECEIPT_EXPORT_AUTHORITY
124
+ emitter: str = "placeholder"
125
+ migration_note: str = PLACEHOLDER_EMITTER_NOTICE
126
+ artifact_slots: dict[str, dict[str, Any]] = field(default_factory=dict)
127
+ evidence_event_mappings: list[dict[str, str]] = field(default_factory=list)
128
+ internal_evidence_event_ids: list[str] = field(default_factory=list)
129
+
130
+ def as_dict(self) -> dict[str, Any]:
131
+ return {
132
+ "operation_id": self.operation_id,
133
+ "authority": self.authority,
134
+ "emitter": self.emitter,
135
+ "migration_note": self.migration_note,
136
+ "artifact_slots": dict(self.artifact_slots),
137
+ "evidence_event_mappings": list(self.evidence_event_mappings),
138
+ "internal_evidence_event_ids": list(self.internal_evidence_event_ids),
139
+ }
140
+
141
+
142
+ class SCLiteEmitter(Protocol):
143
+ def export_operation_receipt(
144
+ self,
145
+ *,
146
+ operation_id: str,
147
+ events: list[dict[str, Any]],
148
+ plan_summary: dict[str, Any] | None = None,
149
+ ) -> SCLiteReceiptExport: ...