coreason-manifest 0.6.0__py3-none-any.whl → 0.7.0__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.
@@ -1,138 +0,0 @@
1
- # Prosperity-3.0
2
- """Policy enforcement functionality using Open Policy Agent (OPA).
3
-
4
- This module provides the `PolicyEnforcer` class, which interacts with OPA to
5
- evaluate agent definitions against compliance policies defined in Rego.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- import json
11
- import shutil
12
- import subprocess
13
- from pathlib import Path
14
- from typing import Any, List, Optional
15
-
16
- from coreason_manifest.errors import PolicyViolationError
17
-
18
-
19
- class PolicyEnforcer:
20
- """Component C: PolicyEnforcer (The Compliance Officer).
21
-
22
- Responsibility:
23
- - Evaluate the agent against the compliance.rego policy file using OPA.
24
- """
25
-
26
- def __init__(
27
- self,
28
- policy_path: str | Path,
29
- opa_path: str = "opa",
30
- data_paths: Optional[List[str | Path]] = None,
31
- ) -> None:
32
- """Initialize the PolicyEnforcer.
33
-
34
- Args:
35
- policy_path: Path to the Rego policy file.
36
- opa_path: Path to the OPA executable. Defaults to "opa" (expected in PATH).
37
- data_paths: List of paths to data files (e.g. JSON/YAML) to be loaded by OPA.
38
-
39
- Raises:
40
- FileNotFoundError: If OPA, policy file, or data files are not found.
41
- """
42
- self.policy_path = Path(policy_path)
43
- self.data_paths = [Path(p) for p in data_paths] if data_paths else []
44
-
45
- # Validate OPA executable
46
- # If opa_path is a simple name (like "opa"), use shutil.which to find it
47
- if "/" not in str(opa_path) and "\\" not in str(opa_path):
48
- resolved_opa: Optional[str] = shutil.which(opa_path)
49
- if not resolved_opa:
50
- raise FileNotFoundError(f"OPA executable not found in PATH: {opa_path}")
51
- self.opa_path: str = resolved_opa
52
- else:
53
- # If it's a path, check existence
54
- if not Path(opa_path).exists():
55
- raise FileNotFoundError(f"OPA executable not found at: {opa_path}")
56
- self.opa_path = str(opa_path)
57
-
58
- if not self.policy_path.exists():
59
- raise FileNotFoundError(f"Policy file not found: {self.policy_path}")
60
-
61
- for path in self.data_paths:
62
- if not path.exists():
63
- raise FileNotFoundError(f"Data file not found: {path}")
64
-
65
- def evaluate(self, agent_data: dict[str, Any]) -> None:
66
- """Evaluates the agent data against the policy.
67
-
68
- Args:
69
- agent_data: The dictionary representation of the AgentDefinition.
70
-
71
- Raises:
72
- PolicyViolationError: If there are any policy violations.
73
- RuntimeError: If OPA execution fails.
74
- """
75
- # Prepare input for OPA
76
- # We invoke OPA via subprocess: opa eval -d <policy> -d <data> ... -I <input> "data.coreason.compliance.deny"
77
- # We pass input via stdin to avoid temp files
78
-
79
- try:
80
- # We use 'data.coreason.compliance.deny' as the query
81
- query = "data.coreason.compliance.deny"
82
-
83
- # Serialize input to JSON
84
- input_json = json.dumps(agent_data)
85
-
86
- cmd = [
87
- self.opa_path,
88
- "eval",
89
- "-d",
90
- str(self.policy_path),
91
- ]
92
-
93
- # Add data paths
94
- for data_path in self.data_paths:
95
- cmd.extend(["-d", str(data_path)])
96
-
97
- cmd.extend(
98
- [
99
- "-I", # Read input from stdin
100
- query,
101
- "--format",
102
- "json",
103
- ]
104
- )
105
-
106
- process = subprocess.run(
107
- cmd,
108
- input=input_json,
109
- capture_output=True,
110
- text=True,
111
- check=False, # We handle return code manually
112
- )
113
-
114
- if process.returncode != 0:
115
- # Include stdout in error message if stderr is empty or insufficient
116
- error_msg = process.stderr.strip()
117
- if not error_msg:
118
- error_msg = process.stdout.strip() or "Unknown error (empty stdout/stderr)"
119
- raise RuntimeError(f"OPA execution failed: {error_msg}")
120
-
121
- # Parse OPA output
122
- # Format: {"result": [{"expressions": [{"value": ["violation1", "violation2"]}]}]}
123
- result = json.loads(process.stdout)
124
-
125
- violations: List[str] = []
126
- if "result" in result and len(result["result"]) > 0:
127
- # Assuming the query returns a set/list of strings
128
- expressions = result["result"][0].get("expressions", [])
129
- if expressions:
130
- violations = expressions[0].get("value", [])
131
-
132
- if violations:
133
- raise PolicyViolationError("Policy violations found.", violations=violations)
134
-
135
- except FileNotFoundError as e:
136
- raise RuntimeError(f"OPA executable not found at: {self.opa_path}") from e
137
- except json.JSONDecodeError as e:
138
- raise RuntimeError(f"Failed to parse OPA output: {e}") from e
@@ -1,123 +0,0 @@
1
- from contextlib import asynccontextmanager
2
- from importlib import resources
3
- from pathlib import Path
4
- from typing import AsyncIterator, List, Optional, Union
5
-
6
- from fastapi import FastAPI, HTTPException, Request, status
7
- from fastapi.responses import JSONResponse
8
- from pydantic import BaseModel, Field
9
-
10
- from coreason_manifest.engine import ManifestConfig, ManifestEngineAsync
11
- from coreason_manifest.errors import ManifestSyntaxError, PolicyViolationError
12
-
13
-
14
- # Response Model
15
- class ValidationResponse(BaseModel):
16
- valid: bool
17
- agent_id: Optional[str] = None
18
- version: Optional[str] = None
19
- policy_violations: List[str] = Field(default_factory=list)
20
-
21
-
22
- @asynccontextmanager
23
- async def lifespan(app: FastAPI) -> AsyncIterator[None]:
24
- # Locate policies
25
- policy_path: Optional[Path] = None
26
- tbom_path: Optional[Path] = None
27
-
28
- # 1. Check local directory (Docker runtime with COPY or relative dev path)
29
- # In Docker we will COPY to /app/policies/ or ./policies/ relative to WORKDIR
30
- # We'll check common locations.
31
- possible_dirs = [
32
- Path("policies"),
33
- Path("/app/policies"),
34
- Path("src/coreason_manifest/policies"), # Dev from root
35
- ]
36
-
37
- for d in possible_dirs:
38
- if (d / "compliance.rego").exists():
39
- policy_path = d / "compliance.rego"
40
- if (d / "tbom.json").exists():
41
- tbom_path = d / "tbom.json"
42
- break
43
-
44
- # 2. Fallback to package resources
45
- resource_context = None
46
- if not policy_path:
47
- try:
48
- # Check if it exists as a resource
49
- ref = resources.files("coreason_manifest.policies").joinpath("compliance.rego")
50
- if ref.is_file():
51
- resource_context = resources.as_file(ref)
52
- policy_path = resource_context.__enter__()
53
- # Check for TBOM in same dir if possible, or ignore for fallback
54
- except Exception:
55
- pass
56
-
57
- # If still not found, fail.
58
- if not policy_path:
59
- raise RuntimeError("Could not locate compliance.rego policy file.")
60
-
61
- config = ManifestConfig(
62
- policy_path=policy_path,
63
- tbom_path=tbom_path,
64
- opa_path="opa", # Assumes OPA is in PATH (installed via Dockerfile)
65
- )
66
-
67
- engine = ManifestEngineAsync(config)
68
-
69
- try:
70
- async with engine:
71
- app.state.engine = engine
72
- yield
73
- finally:
74
- if resource_context:
75
- resource_context.__exit__(None, None, None)
76
-
77
-
78
- app = FastAPI(lifespan=lifespan)
79
-
80
-
81
- @app.post("/validate", response_model=ValidationResponse) # type: ignore[misc]
82
- async def validate_manifest(request: Request) -> Union[ValidationResponse, JSONResponse]:
83
- engine: ManifestEngineAsync = app.state.engine
84
-
85
- try:
86
- raw_body = await request.json()
87
- except Exception:
88
- raise HTTPException(status_code=400, detail="Invalid JSON body") from None
89
-
90
- try:
91
- agent_def = await engine.validate_manifest_dict(raw_body)
92
- return ValidationResponse(
93
- valid=True,
94
- agent_id=str(agent_def.metadata.id),
95
- version=agent_def.metadata.version,
96
- policy_violations=[],
97
- )
98
- except ManifestSyntaxError as e:
99
- # Return 422 with the error
100
- resp = ValidationResponse(valid=False, policy_violations=[f"Syntax Error: {str(e)}"])
101
- return JSONResponse(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, content=resp.model_dump())
102
- except PolicyViolationError as e:
103
- resp = ValidationResponse(valid=False, policy_violations=e.violations)
104
- return JSONResponse(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, content=resp.model_dump())
105
-
106
-
107
- @app.get("/health") # type: ignore[misc]
108
- async def health_check() -> dict[str, str]:
109
- engine: ManifestEngineAsync = app.state.engine
110
- policy_version = "unknown"
111
- try:
112
- import hashlib
113
-
114
- # policy_path is guaranteed to exist by lifespan check
115
- policy_path = Path(engine.config.policy_path)
116
- if policy_path.exists():
117
- with open(policy_path, "rb") as f:
118
- digest = hashlib.sha256(f.read()).hexdigest()[:8]
119
- policy_version = digest
120
- except Exception:
121
- pass
122
-
123
- return {"status": "active", "policy_version": policy_version}
@@ -1,67 +0,0 @@
1
- # Prosperity-3.0
2
- """Schema validation functionality.
3
-
4
- This module provides the `SchemaValidator` class, which uses JSON Schema to
5
- validate the structure and types of agent definitions.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- import json
11
- from importlib.resources import files
12
- from typing import Any
13
-
14
- from jsonschema import FormatChecker, ValidationError, validate
15
-
16
- from coreason_manifest.errors import ManifestSyntaxError
17
-
18
-
19
- class SchemaValidator:
20
- """Component B: SchemaValidator (The Structural Engineer).
21
-
22
- Responsibility:
23
- - Validate the dictionary against the Master JSON Schema.
24
- - Check required fields, data types, and format constraints.
25
- """
26
-
27
- def __init__(self) -> None:
28
- """Initialize the validator by loading the schema."""
29
- self.schema = self._load_schema()
30
-
31
- def _load_schema(self) -> dict[str, Any]:
32
- """Loads the JSON schema from the package resources.
33
-
34
- Returns:
35
- The JSON schema dictionary.
36
-
37
- Raises:
38
- ManifestSyntaxError: If the schema file cannot be loaded or is invalid.
39
- """
40
- try:
41
- schema_path = files("coreason_manifest.schemas").joinpath("agent.schema.json")
42
- with schema_path.open("r", encoding="utf-8") as f:
43
- schema = json.load(f)
44
- if not isinstance(schema, dict):
45
- raise ManifestSyntaxError("Schema file is not a valid JSON object.")
46
- return schema
47
- except (IOError, json.JSONDecodeError) as e:
48
- raise ManifestSyntaxError(f"Failed to load agent schema: {e}") from e
49
-
50
- def validate(self, data: dict[str, Any]) -> bool:
51
- """Validates the given dictionary against the agent schema.
52
-
53
- Args:
54
- data: The raw dictionary to validate.
55
-
56
- Returns:
57
- True if validation passes.
58
-
59
- Raises:
60
- ManifestSyntaxError: If validation fails.
61
- """
62
- try:
63
- validate(instance=data, schema=self.schema, format_checker=FormatChecker())
64
- return True
65
- except ValidationError as e:
66
- # We treat schema validation errors as syntax errors in the manifest
67
- raise ManifestSyntaxError(f"Schema validation failed: {e.message}") from e
@@ -1,22 +0,0 @@
1
- coreason_manifest/__init__.py,sha256=wDlmlYxr29P_fX5Et6pP-jcMtQe6eTzKvpUrh7zFMOM,1857
2
- coreason_manifest/engine.py,sha256=ENAkRuGFd1uY_5u4EYEsTZUMrqw5HgijKtkF9lk-E-c,8199
3
- coreason_manifest/errors.py,sha256=CKFWSucncoPLYUJcsuXtLIfneyr2d89oHRTiWPT4psU,1437
4
- coreason_manifest/integrity.py,sha256=CMBaa2A8DpC9hDSO-aj-YOzss6if1CajaeLEmTC703c,5344
5
- coreason_manifest/loader.py,sha256=WET8HAnSw4dgon_zdiZaaU1dIUrsIQPYZzxlp0crTYo,10027
6
- coreason_manifest/main.py,sha256=YQO98w2wxkfNs-DpSLmiDTs9DtUeGsC48GEfbElXVPk,357
7
- coreason_manifest/models.py,sha256=EuaE1Wl5T8f_eI9vF1VFp5hd0y9C7E52zAjB52EulKw,7497
8
- coreason_manifest/policies/compliance.rego,sha256=-drMuno6YkGOXKjvdLWawCZWK8iUxY7OcXpoXa_9Oyo,3035
9
- coreason_manifest/policies/tbom.json,sha256=rSn4V44_IdFqCC86J3Jc31qQKTV4J5BdmyO0CI4iOu0,167
10
- coreason_manifest/policy.py,sha256=vvEivq5HSjv-bMSrZ5VMM3eTomYFp39SBtuFajG9RU4,5009
11
- coreason_manifest/recipes.py,sha256=tMNOE-JYJtG6NYRH1a-IoCLD4gXo7qoTb8dypLkmHC0,5849
12
- coreason_manifest/schemas/__init__.py,sha256=9TMs6jWKCIewAKkj-u0tq9c6N_-0CU6b5s9q6MTS6v4,17
13
- coreason_manifest/schemas/agent.schema.json,sha256=VigUX3ltX7YaW9P7n0DNYGNOf8C6VCuXNy71u3LB9i4,6812
14
- coreason_manifest/server.py,sha256=ezuk-4sABF92U4cY1ppYJGJf8q6yeoteVlrwnmk7Jis,4253
15
- coreason_manifest/utils/__init__.py,sha256=Q9gXiBtX3mD9GTu4z0JDHSHkbXC-MRHagrOaOmRH_1Q,435
16
- coreason_manifest/utils/logger.py,sha256=A7E6Hd_Jk1XDUajNEJQl-WtUv9M2LT76b4_TsbxnILw,1227
17
- coreason_manifest/validator.py,sha256=v33EzKroRwLEjZeuRRpTB7cqB38op3DV2EZpZhJ80a0,2240
18
- coreason_manifest-0.6.0.dist-info/METADATA,sha256=nkC0JpE_Itg26p5U7-mS8KizLv-lBc9eZM2FrB3oioM,7421
19
- coreason_manifest-0.6.0.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
20
- coreason_manifest-0.6.0.dist-info/licenses/LICENSE,sha256=3tYb7ZQe7sVXcbNmX22fDESFjOSIlCZodUGpZMkuSlk,3063
21
- coreason_manifest-0.6.0.dist-info/licenses/NOTICE,sha256=tqzUyP9VTCGxoHLgBI0AC1i0G7m_PSyESFL8Jwuw0dA,610
22
- coreason_manifest-0.6.0.dist-info/RECORD,,
File without changes