coreason-manifest 0.6.0__tar.gz → 0.7.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 (27) hide show
  1. {coreason_manifest-0.6.0 → coreason_manifest-0.7.0}/PKG-INFO +1 -7
  2. {coreason_manifest-0.6.0 → coreason_manifest-0.7.0}/pyproject.toml +3 -9
  3. coreason_manifest-0.7.0/src/coreason_manifest/__init__.py +17 -0
  4. coreason_manifest-0.7.0/src/coreason_manifest/definitions/__init__.py +0 -0
  5. coreason_manifest-0.7.0/src/coreason_manifest/definitions/audit.py +7 -0
  6. coreason_manifest-0.7.0/src/coreason_manifest/definitions/simulation.py +19 -0
  7. coreason_manifest-0.6.0/src/coreason_manifest/recipes.py → coreason_manifest-0.7.0/src/coreason_manifest/definitions/topology.py +2 -23
  8. coreason_manifest-0.7.0/src/coreason_manifest/recipes.py +38 -0
  9. coreason_manifest-0.6.0/src/coreason_manifest/__init__.py +0 -79
  10. coreason_manifest-0.6.0/src/coreason_manifest/engine.py +0 -222
  11. coreason_manifest-0.6.0/src/coreason_manifest/errors.py +0 -53
  12. coreason_manifest-0.6.0/src/coreason_manifest/integrity.py +0 -141
  13. coreason_manifest-0.6.0/src/coreason_manifest/loader.py +0 -271
  14. coreason_manifest-0.6.0/src/coreason_manifest/main.py +0 -17
  15. coreason_manifest-0.6.0/src/coreason_manifest/policies/compliance.rego +0 -81
  16. coreason_manifest-0.6.0/src/coreason_manifest/policies/tbom.json +0 -14
  17. coreason_manifest-0.6.0/src/coreason_manifest/policy.py +0 -138
  18. coreason_manifest-0.6.0/src/coreason_manifest/server.py +0 -123
  19. coreason_manifest-0.6.0/src/coreason_manifest/validator.py +0 -67
  20. {coreason_manifest-0.6.0 → coreason_manifest-0.7.0}/LICENSE +0 -0
  21. {coreason_manifest-0.6.0 → coreason_manifest-0.7.0}/NOTICE +0 -0
  22. {coreason_manifest-0.6.0 → coreason_manifest-0.7.0}/README.md +0 -0
  23. /coreason_manifest-0.6.0/src/coreason_manifest/models.py → /coreason_manifest-0.7.0/src/coreason_manifest/definitions/agent.py +0 -0
  24. {coreason_manifest-0.6.0 → coreason_manifest-0.7.0}/src/coreason_manifest/schemas/__init__.py +0 -0
  25. {coreason_manifest-0.6.0 → coreason_manifest-0.7.0}/src/coreason_manifest/schemas/agent.schema.json +0 -0
  26. {coreason_manifest-0.6.0 → coreason_manifest-0.7.0}/src/coreason_manifest/utils/__init__.py +0 -0
  27. {coreason_manifest-0.6.0 → coreason_manifest-0.7.0}/src/coreason_manifest/utils/logger.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coreason_manifest
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: This package is the definitive source of truth. If it isn't in the manifest, it doesn't exist. If it violates the manifest, it doesn't run.
5
5
  License: # The Prosperity Public License 3.0.0
6
6
 
@@ -67,16 +67,10 @@ Requires-Python: >=3.12
67
67
  Classifier: License :: Other/Proprietary License
68
68
  Classifier: Programming Language :: Python :: 3.12
69
69
  Classifier: Operating System :: OS Independent
70
- Requires-Dist: aiofiles (>=23.2.1,<24.0.0)
71
- Requires-Dist: anyio (>=4.12.1,<5.0.0)
72
70
  Requires-Dist: coreason-identity (>=0.4.1,<0.5.0)
73
- Requires-Dist: fastapi (>=0.128.0,<0.129.0)
74
- Requires-Dist: httpx (>=0.28.1,<0.29.0)
75
- Requires-Dist: jsonschema (>=4.26.0,<5.0.0)
76
71
  Requires-Dist: loguru (>=0.7.2,<0.8.0)
77
72
  Requires-Dist: pydantic (>=2.12.5,<3.0.0)
78
73
  Requires-Dist: pyyaml (>=6.0.3,<7.0.0)
79
- Requires-Dist: uvicorn (>=0.40.0,<0.41.0)
80
74
  Project-URL: Documentation, https://github.com/CoReason-AI/coreason_manifest
81
75
  Project-URL: Homepage, https://github.com/CoReason-AI/coreason_manifest
82
76
  Project-URL: Repository, https://github.com/CoReason-AI/coreason_manifest
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "coreason_manifest"
3
- version = "0.6.0"
3
+ version = "0.7.0"
4
4
  description = "This package is the definitive source of truth. If it isn't in the manifest, it doesn't exist. If it violates the manifest, it doesn't run."
5
5
  authors = ["Gowtham A Rao <gowtham.rao@coreason.ai>"]
6
6
  license = "Prosperity-3.0"
@@ -11,14 +11,8 @@ packages = [{include = "coreason_manifest", from = "src"}]
11
11
  python = ">=3.12, <3.15"
12
12
  loguru = "^0.7.2"
13
13
  pydantic = "^2.12.5"
14
- jsonschema = "^4.26.0"
15
14
  pyyaml = "^6.0.3"
16
- anyio = "^4.12.1"
17
15
  coreason-identity = "^0.4.1"
18
- httpx = "^0.28.1"
19
- aiofiles = "^23.2.1"
20
- fastapi = "^0.128.0"
21
- uvicorn = "^0.40.0"
22
16
 
23
17
  [tool.poetry.group.dev.dependencies]
24
18
  pytest = "^9.0.2"
@@ -29,8 +23,8 @@ mkdocs = "^1.6.1"
29
23
  mkdocs-material = "^9.7.1"
30
24
  pydantic = "^2.12.5"
31
25
  mypy = "^1.19.1"
32
- types-aiofiles = "^23.2.0"
33
26
  pytest-asyncio = "^1.3.0"
27
+ types-pyyaml = "^6.0.12.20250915"
34
28
 
35
29
  [build-system]
36
30
  requires = ["poetry-core"]
@@ -38,7 +32,7 @@ build-backend = "poetry.core.masonry.api"
38
32
 
39
33
  [project]
40
34
  name = "coreason_manifest"
41
- version = "0.6.0"
35
+ version = "0.7.0"
42
36
  description = "This package is the definitive source of truth. If it isn't in the manifest, it doesn't exist. If it violates the manifest, it doesn't run."
43
37
  readme = "README.md"
44
38
  requires-python = ">=3.12"
@@ -0,0 +1,17 @@
1
+ from .definitions.agent import AgentDefinition
2
+ from .definitions.audit import AuditLog
3
+ from .definitions.simulation import SimulationScenario, SimulationTrace, SimulationTurn
4
+ from .definitions.topology import Edge, Node, Topology
5
+ from .recipes import RecipeManifest
6
+
7
+ __all__ = [
8
+ "AgentDefinition",
9
+ "Topology",
10
+ "Node",
11
+ "Edge",
12
+ "SimulationScenario",
13
+ "SimulationTrace",
14
+ "SimulationTurn",
15
+ "AuditLog",
16
+ "RecipeManifest",
17
+ ]
@@ -0,0 +1,7 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class AuditLog(BaseModel):
5
+ """Audit log entry."""
6
+
7
+ pass
@@ -0,0 +1,19 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class SimulationScenario(BaseModel):
5
+ """Definition of a simulation scenario."""
6
+
7
+ pass
8
+
9
+
10
+ class SimulationTrace(BaseModel):
11
+ """Trace of a simulation execution."""
12
+
13
+ pass
14
+
15
+
16
+ class SimulationTurn(BaseModel):
17
+ """A single turn in a simulation."""
18
+
19
+ pass
@@ -8,12 +8,10 @@
8
8
  #
9
9
  # Source Code: https://github.com/CoReason-AI/coreason_maco
10
10
 
11
- from typing import Annotated, Any, Dict, List, Literal, Optional, Union
11
+ from typing import Annotated, List, Literal, Optional, Union
12
12
 
13
13
  from pydantic import BaseModel, ConfigDict, Field
14
14
 
15
- from .models import VersionStr
16
-
17
15
 
18
16
  class CouncilConfig(BaseModel):
19
17
  """Configuration for 'Architectural Triangulation'.
@@ -139,23 +137,4 @@ class GraphTopology(BaseModel):
139
137
  edges: List[Edge] = Field(..., description="List of edges connecting the nodes.")
140
138
 
141
139
 
142
- class RecipeManifest(BaseModel):
143
- """The executable specification for the MACO engine.
144
-
145
- Attributes:
146
- id: Unique identifier for the recipe.
147
- version: Version of the recipe.
148
- name: Human-readable name of the recipe.
149
- description: Detailed description of the recipe.
150
- inputs: Schema defining global variables this recipe accepts.
151
- graph: The topology definition of the workflow.
152
- """
153
-
154
- model_config = ConfigDict(extra="forbid")
155
-
156
- id: str = Field(..., description="Unique identifier for the recipe.")
157
- version: VersionStr = Field(..., description="Version of the recipe.")
158
- name: str = Field(..., description="Human-readable name of the recipe.")
159
- description: Optional[str] = Field(None, description="Detailed description of the recipe.")
160
- inputs: Dict[str, Any] = Field(..., description="Schema defining global variables this recipe accepts.")
161
- graph: GraphTopology = Field(..., description="The topology definition of the workflow.")
140
+ Topology = GraphTopology
@@ -0,0 +1,38 @@
1
+ # Copyright (c) 2025 CoReason, Inc.
2
+ #
3
+ # This software is proprietary and dual-licensed.
4
+ # Licensed under the Prosperity Public License 3.0 (the "License").
5
+ # A copy of the license is available at https://prosperitylicense.com/versions/3.0.0
6
+ # For details, see the LICENSE file.
7
+ # Commercial use beyond a 30-day trial requires a separate license.
8
+ #
9
+ # Source Code: https://github.com/CoReason-AI/coreason_maco
10
+
11
+ from typing import Any, Dict, Optional
12
+
13
+ from pydantic import BaseModel, ConfigDict, Field
14
+
15
+ from .definitions.agent import VersionStr
16
+ from .definitions.topology import GraphTopology
17
+
18
+
19
+ class RecipeManifest(BaseModel):
20
+ """The executable specification for the MACO engine.
21
+
22
+ Attributes:
23
+ id: Unique identifier for the recipe.
24
+ version: Version of the recipe.
25
+ name: Human-readable name of the recipe.
26
+ description: Detailed description of the recipe.
27
+ inputs: Schema defining global variables this recipe accepts.
28
+ graph: The topology definition of the workflow.
29
+ """
30
+
31
+ model_config = ConfigDict(extra="forbid")
32
+
33
+ id: str = Field(..., description="Unique identifier for the recipe.")
34
+ version: VersionStr = Field(..., description="Version of the recipe.")
35
+ name: str = Field(..., description="Human-readable name of the recipe.")
36
+ description: Optional[str] = Field(None, description="Detailed description of the recipe.")
37
+ inputs: Dict[str, Any] = Field(..., description="Schema defining global variables this recipe accepts.")
38
+ graph: GraphTopology = Field(..., description="The topology definition of the workflow.")
@@ -1,79 +0,0 @@
1
- # Prosperity-3.0
2
- """Coreason Manifest Package.
3
-
4
- This package provides the core functionality for the Coreason Manifest system,
5
- including loading, validation, policy enforcement, and integrity checking of
6
- agent definitions.
7
-
8
- The `coreason-manifest` package serves as the definitive source of truth for
9
- Asset definitions in the CoReason-AI ecosystem.
10
-
11
- Usage:
12
- from coreason_manifest import ManifestEngine, ManifestConfig
13
-
14
- config = ManifestConfig(policy_path="./policies/gx_compliant.rego")
15
- engine = ManifestEngine(config)
16
- agent_def = engine.load_and_validate("agent.yaml", "./src")
17
- """
18
-
19
- from .engine import ManifestConfig, ManifestEngine, ManifestEngineAsync
20
- from .errors import (
21
- IntegrityCompromisedError,
22
- ManifestError,
23
- ManifestSyntaxError,
24
- PolicyViolationError,
25
- )
26
- from .integrity import IntegrityChecker
27
- from .loader import ManifestLoader
28
- from .models import (
29
- AgentDefinition,
30
- AgentDependencies,
31
- AgentInterface,
32
- AgentMetadata,
33
- AgentTopology,
34
- ModelConfig,
35
- Step,
36
- )
37
- from .policy import PolicyEnforcer
38
- from .recipes import (
39
- AgentNode,
40
- CouncilConfig,
41
- Edge,
42
- GraphTopology,
43
- HumanNode,
44
- LogicNode,
45
- Node,
46
- RecipeManifest,
47
- VisualMetadata,
48
- )
49
- from .validator import SchemaValidator
50
-
51
- __all__ = [
52
- "AgentDefinition",
53
- "AgentDependencies",
54
- "AgentInterface",
55
- "AgentMetadata",
56
- "AgentNode",
57
- "AgentTopology",
58
- "CouncilConfig",
59
- "Edge",
60
- "GraphTopology",
61
- "HumanNode",
62
- "IntegrityChecker",
63
- "IntegrityCompromisedError",
64
- "LogicNode",
65
- "ManifestConfig",
66
- "ManifestEngine",
67
- "ManifestEngineAsync",
68
- "ManifestError",
69
- "ManifestLoader",
70
- "ManifestSyntaxError",
71
- "ModelConfig",
72
- "Node",
73
- "PolicyEnforcer",
74
- "PolicyViolationError",
75
- "RecipeManifest",
76
- "SchemaValidator",
77
- "Step",
78
- "VisualMetadata",
79
- ]
@@ -1,222 +0,0 @@
1
- # Prosperity-3.0
2
- """Engine for the Coreason Manifest system.
3
-
4
- This module provides the main entry point for verifying and loading Agent Manifests.
5
- It coordinates schema validation, policy enforcement, and integrity checking.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- import time
11
- from dataclasses import dataclass, field
12
- from pathlib import Path
13
- from typing import Any, List, Optional, Union, cast
14
-
15
- import anyio
16
- import anyio.to_thread
17
-
18
- from coreason_manifest.integrity import IntegrityChecker
19
- from coreason_manifest.loader import ManifestLoader
20
- from coreason_manifest.models import AgentDefinition
21
- from coreason_manifest.policy import PolicyEnforcer
22
- from coreason_manifest.utils.logger import logger
23
- from coreason_manifest.validator import SchemaValidator
24
-
25
-
26
- @dataclass
27
- class ManifestConfig:
28
- """Configuration for the ManifestEngine.
29
-
30
- Attributes:
31
- policy_path: Path to the Rego policy file.
32
- opa_path: Path to the OPA executable. Defaults to "opa".
33
- tbom_path: Optional path to the Trusted Bill of Materials.
34
- extra_data_paths: Additional data paths to load into OPA.
35
- """
36
-
37
- policy_path: Union[str, Path]
38
- opa_path: str = "opa"
39
- tbom_path: Optional[Union[str, Path]] = None
40
- extra_data_paths: List[Union[str, Path]] = field(default_factory=list)
41
-
42
-
43
- class ManifestEngineAsync:
44
- """The async core for verifying and loading Agent Manifests.
45
-
46
- This class coordinates the validation process, including:
47
- 1. Loading raw YAML.
48
- 2. Validating against JSON Schema.
49
- 3. Converting to AgentDefinition Pydantic model (Normalization).
50
- 4. Enforcing Policy (Rego).
51
- 5. Verifying Integrity (Hash check).
52
- """
53
-
54
- def __init__(self, config: ManifestConfig) -> None:
55
- """Initialize the ManifestEngineAsync.
56
-
57
- Args:
58
- config: Configuration including policy path and OPA path.
59
- """
60
- self.config = config
61
- self.schema_validator = SchemaValidator()
62
-
63
- # Collect data paths
64
- data_paths = list(config.extra_data_paths)
65
- if config.tbom_path:
66
- data_paths.append(config.tbom_path)
67
-
68
- self.policy_enforcer = PolicyEnforcer(
69
- policy_path=config.policy_path,
70
- opa_path=config.opa_path,
71
- data_paths=data_paths,
72
- )
73
-
74
- async def __aenter__(self) -> ManifestEngineAsync:
75
- """Async context manager entry."""
76
- return self
77
-
78
- async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
79
- """Async context manager exit."""
80
- # Clean up resources if necessary.
81
- pass
82
-
83
- async def validate_manifest_dict(self, raw_data: dict[str, Any]) -> AgentDefinition:
84
- """Validates an Agent Manifest dictionary in memory.
85
-
86
- Performs:
87
- 1. Normalization (stripping version prefixes)
88
- 2. Schema Validation
89
- 3. Model Conversion
90
- 4. Policy Enforcement
91
-
92
- Does NOT perform Integrity Check (hashing).
93
-
94
- Args:
95
- raw_data: The raw dictionary of the manifest.
96
-
97
- Returns:
98
- AgentDefinition: The fully validated agent definition.
99
-
100
- Raises:
101
- ManifestSyntaxError: If structure or schema is invalid.
102
- PolicyViolationError: If business rules are violated.
103
- """
104
- # 1. Normalization (ensure version string is clean before schema/model validation)
105
- # We access the static method on ManifestLoader.
106
- ManifestLoader._normalize_data(raw_data)
107
-
108
- # 2. Schema Validation
109
- logger.debug("Running Schema Validation...")
110
- self.schema_validator.validate(raw_data)
111
-
112
- # 3. Model Conversion (Normalization) (CPU bound)
113
- logger.debug("Converting to AgentDefinition...")
114
- agent_def = await anyio.to_thread.run_sync(ManifestLoader.load_from_dict, raw_data)
115
- logger.info(f"Validating Agent {agent_def.metadata.id} v{agent_def.metadata.version}")
116
-
117
- # 4. Policy Enforcement (Subprocess / Blocking)
118
- logger.debug("Enforcing Policies...")
119
- # We assume policy is checked against the Normalized data (model dumped back to dict)
120
- normalized_data = agent_def.model_dump(mode="json")
121
- start_time = time.perf_counter()
122
- try:
123
- # PolicyEnforcer.evaluate is synchronous and runs subprocess.run, so we wrap it.
124
- await anyio.to_thread.run_sync(self.policy_enforcer.evaluate, normalized_data)
125
- duration_ms = (time.perf_counter() - start_time) * 1000
126
- logger.info(f"Policy Check: Pass - {duration_ms:.2f}ms")
127
- except Exception:
128
- duration_ms = (time.perf_counter() - start_time) * 1000
129
- logger.info(f"Policy Check: Fail - {duration_ms:.2f}ms")
130
- raise
131
-
132
- return cast(AgentDefinition, agent_def)
133
-
134
- async def load_and_validate(self, manifest_path: Union[str, Path], source_dir: Union[str, Path]) -> AgentDefinition:
135
- """Loads, validates, and verifies an Agent Manifest asynchronously.
136
-
137
- Args:
138
- manifest_path: Path to the agent.yaml file.
139
- source_dir: Path to the source code directory.
140
-
141
- Returns:
142
- AgentDefinition: The fully validated and verified agent definition.
143
-
144
- Raises:
145
- ManifestSyntaxError: If structure or schema is invalid.
146
- PolicyViolationError: If business rules are violated.
147
- IntegrityCompromisedError: If source code hash does not match.
148
- FileNotFoundError: If files are missing.
149
- """
150
- manifest_path = Path(manifest_path)
151
- source_dir = Path(source_dir)
152
-
153
- logger.info(f"Validating Agent Manifest: {manifest_path}")
154
-
155
- # 1. Load Raw YAML (I/O)
156
- raw_data = await ManifestLoader.load_raw_from_file_async(manifest_path)
157
-
158
- # 2. Validate Manifest Dict (Schema, Model, Policy)
159
- agent_def = await self.validate_manifest_dict(raw_data)
160
-
161
- # 5. Integrity Check (Heavy I/O and CPU)
162
- logger.debug("Verifying Integrity...")
163
- # IntegrityChecker.verify is synchronous and does heavy IO, so we wrap it.
164
- await anyio.to_thread.run_sync(IntegrityChecker.verify, agent_def, source_dir, manifest_path)
165
-
166
- logger.info("Agent validation successful.")
167
- return agent_def
168
-
169
-
170
- class ManifestEngine:
171
- """The Sync Facade for ManifestEngineAsync.
172
-
173
- Allows synchronous usage of the async core via anyio.run.
174
- """
175
-
176
- def __init__(self, config: ManifestConfig) -> None:
177
- """Initialize the ManifestEngine facade.
178
-
179
- Args:
180
- config: Configuration including policy path and OPA path.
181
- """
182
- self._async = ManifestEngineAsync(config)
183
-
184
- def __getattr__(self, name: str) -> Any:
185
- """Delegate attribute access to the async engine instance.
186
-
187
- This ensures backward compatibility for accessing attributes like
188
- 'config', 'schema_validator', and 'policy_enforcer'.
189
- """
190
- return getattr(self._async, name)
191
-
192
- def __enter__(self) -> ManifestEngine:
193
- """Context manager entry."""
194
- anyio.run(self._async.__aenter__)
195
- return self
196
-
197
- def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
198
- """Context manager exit."""
199
- anyio.run(self._async.__aexit__, exc_type, exc_val, exc_tb)
200
-
201
- def load_and_validate(self, manifest_path: Union[str, Path], source_dir: Union[str, Path]) -> AgentDefinition:
202
- """Loads, validates, and verifies an Agent Manifest synchronously.
203
-
204
- Args:
205
- manifest_path: Path to the agent.yaml file.
206
- source_dir: Path to the source code directory.
207
-
208
- Returns:
209
- AgentDefinition: The fully validated and verified agent definition.
210
- """
211
- return cast(AgentDefinition, anyio.run(self._async.load_and_validate, manifest_path, source_dir))
212
-
213
- def validate_manifest_dict(self, raw_data: dict[str, Any]) -> AgentDefinition:
214
- """Validates an Agent Manifest dictionary synchronously.
215
-
216
- Args:
217
- raw_data: The raw dictionary of the manifest.
218
-
219
- Returns:
220
- AgentDefinition: The fully validated agent definition.
221
- """
222
- return cast(AgentDefinition, anyio.run(self._async.validate_manifest_dict, raw_data))
@@ -1,53 +0,0 @@
1
- # Prosperity-3.0
2
- """Exceptions for the Coreason Manifest system.
3
-
4
- This module defines the hierarchy of exceptions raised by the package.
5
- """
6
-
7
- from __future__ import annotations
8
-
9
-
10
- class ManifestError(Exception):
11
- """Base exception for coreason_manifest errors."""
12
-
13
- pass
14
-
15
-
16
- class ManifestSyntaxError(ManifestError):
17
- """Raised when the manifest YAML is invalid or missing required fields.
18
-
19
- This includes YAML parsing errors and JSON Schema validation failures.
20
- """
21
-
22
- pass
23
-
24
-
25
- class PolicyViolationError(ManifestError):
26
- """Raised when the agent violates a compliance policy.
27
-
28
- This error indicates that the manifest is structurally valid but fails
29
- business rules or compliance checks (e.g., banned libraries).
30
-
31
- Attributes:
32
- violations: A list of specific policy violation messages.
33
- """
34
-
35
- def __init__(self, message: str, violations: list[str] | None = None) -> None:
36
- """Initialize PolicyViolationError.
37
-
38
- Args:
39
- message: The error message.
40
- violations: Optional list of detailed violation strings.
41
- """
42
- super().__init__(message)
43
- self.violations = violations or []
44
-
45
-
46
- class IntegrityCompromisedError(ManifestError):
47
- """Raised when the source code hash does not match the manifest.
48
-
49
- This indicates that the source code may have been tampered with or changed
50
- without updating the manifest's integrity hash.
51
- """
52
-
53
- pass
@@ -1,141 +0,0 @@
1
- # Prosperity-3.0
2
- """Integrity checking functionality.
3
-
4
- This module provides the `IntegrityChecker` class, which is responsible for
5
- calculating deterministic hashes of source code directories and verifying
6
- them against the expected hash in the agent manifest.
7
- """
8
-
9
- from __future__ import annotations
10
-
11
- import hashlib
12
- import os
13
- from pathlib import Path
14
- from typing import List, Optional, Set, Union
15
-
16
- from coreason_manifest.errors import IntegrityCompromisedError
17
- from coreason_manifest.models import AgentDefinition
18
-
19
-
20
- class IntegrityChecker:
21
- """Component D: IntegrityChecker (The Notary).
22
-
23
- Responsibility:
24
- - Calculate the SHA256 hash of the source code directory.
25
- - Compare it against the integrity_hash defined in the manifest.
26
- """
27
-
28
- IGNORED_DIRS = frozenset({".git", "__pycache__", ".venv", ".env", ".DS_Store"})
29
-
30
- @staticmethod
31
- def calculate_hash(source_dir: Union[Path, str], exclude_files: Optional[Set[Union[Path, str]]] = None) -> str:
32
- """Calculates a deterministic SHA256 hash of the source code directory.
33
-
34
- It walks the directory using os.walk to efficiently prune ignored directories.
35
- Sorts files by relative path, hashes each file, and then hashes the sequence.
36
-
37
- Ignores hidden directories/files in IGNORED_DIRS.
38
- Rejects symbolic links for security.
39
-
40
- Args:
41
- source_dir: The directory containing source code.
42
- exclude_files: Optional set of file paths (absolute or relative to CWD)
43
- to exclude from hashing.
44
-
45
- Returns:
46
- The hex digest of the SHA256 hash.
47
-
48
- Raises:
49
- FileNotFoundError: If source_dir does not exist.
50
- IntegrityCompromisedError: If a symlink is found.
51
- """
52
- path_obj = Path(source_dir)
53
- if path_obj.is_symlink():
54
- raise IntegrityCompromisedError(f"Symbolic links are forbidden: {path_obj}")
55
-
56
- source_path = path_obj.resolve()
57
- if not source_path.exists():
58
- raise FileNotFoundError(f"Source directory not found: {source_path}")
59
-
60
- # Normalize excluded files to absolute paths
61
- excludes = set()
62
- if exclude_files:
63
- for ex_path in exclude_files:
64
- excludes.add(Path(ex_path).resolve())
65
-
66
- sha256 = hashlib.sha256()
67
- file_paths: List[Path] = []
68
-
69
- # Use os.walk for efficient traversal and pruning
70
- for root, dirs, files in os.walk(source_path, topdown=True):
71
- root_path = Path(root)
72
-
73
- # Check for symlinks in directories before pruning
74
- for d_name in dirs:
75
- d_path = root_path / d_name
76
- if d_path.is_symlink():
77
- raise IntegrityCompromisedError(f"Symbolic links are forbidden: {d_path}") # pragma: no cover
78
-
79
- # Prune directories efficiently using slice assignment
80
- dirs[:] = [d for d in dirs if d not in IntegrityChecker.IGNORED_DIRS]
81
-
82
- # Collect files
83
- for f_name in files:
84
- f_path = root_path / f_name
85
-
86
- if f_path.is_symlink():
87
- raise IntegrityCompromisedError(f"Symbolic links are forbidden: {f_path}")
88
-
89
- if f_name in IntegrityChecker.IGNORED_DIRS:
90
- continue
91
-
92
- # Use resolved path for exclusion checking and inclusion
93
- f_path_abs = f_path.resolve()
94
- if f_path_abs in excludes:
95
- continue
96
-
97
- file_paths.append(f_path_abs)
98
-
99
- # Sort to ensure deterministic order
100
- # Use as_posix() to ensure ASCII sorting (case-sensitive) on all platforms (Windows vs Linux)
101
- file_paths.sort(key=lambda p: p.relative_to(source_path).as_posix())
102
-
103
- for path in file_paths:
104
- # Update hash with relative path to ensure structure matters
105
- # Use forward slashes for cross-platform consistency
106
- rel_path = path.relative_to(source_path).as_posix().encode("utf-8")
107
- sha256.update(rel_path)
108
-
109
- # Update hash with file content
110
- with open(path, "rb") as f:
111
- while chunk := f.read(8192):
112
- sha256.update(chunk)
113
-
114
- return sha256.hexdigest()
115
-
116
- @staticmethod
117
- def verify(
118
- agent_def: AgentDefinition,
119
- source_dir: Union[Path, str],
120
- manifest_path: Optional[Union[Path, str]] = None,
121
- ) -> None:
122
- """Verifies the integrity of the source code against the manifest.
123
-
124
- Args:
125
- agent_def: The AgentDefinition containing the expected hash.
126
- source_dir: The directory containing source code.
127
- manifest_path: Optional path to the manifest file to exclude from hashing.
128
-
129
- Raises:
130
- IntegrityCompromisedError: If the hash does not match or is missing.
131
- FileNotFoundError: If source_dir does not exist.
132
- """
133
- exclude_files = {manifest_path} if manifest_path else None
134
-
135
- # agent_def.integrity_hash is now required by Pydantic model
136
- calculated = IntegrityChecker.calculate_hash(source_dir, exclude_files=exclude_files)
137
-
138
- if calculated != agent_def.integrity_hash:
139
- raise IntegrityCompromisedError(
140
- f"Integrity check failed. Expected {agent_def.integrity_hash}, got {calculated}"
141
- )