coreason-manifest 0.6.0__py3-none-any.whl → 0.9.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,271 +0,0 @@
1
- # Prosperity-3.0
2
- """Manifest Loader.
3
-
4
- This module is responsible for loading the agent manifest from YAML files or
5
- dictionaries, normalizing the data, and converting it into Pydantic models.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- import inspect
11
- from pathlib import Path
12
- from typing import Any, Callable, Dict, List, Union, get_type_hints
13
-
14
- try:
15
- from typing import get_args, get_origin
16
- except ImportError: # pragma: no cover
17
- # For Python < 3.8, though project requires 3.12+
18
- from typing_extensions import get_args, get_origin # type: ignore
19
-
20
- import aiofiles
21
- import yaml
22
- from coreason_identity import UserContext
23
- from pydantic import ValidationError, create_model
24
-
25
- from coreason_manifest.errors import ManifestSyntaxError
26
- from coreason_manifest.models import AgentDefinition, AgentInterface
27
-
28
-
29
- class ManifestLoader:
30
- """Component A: ManifestLoader (The Parser).
31
-
32
- Responsibility:
33
- - Load YAML safely.
34
- - Convert raw data into a Pydantic AgentDefinition model.
35
- - Normalization: Ensure all version strings follow SemVer and all IDs are canonical UUIDs.
36
- """
37
-
38
- @staticmethod
39
- def load_raw_from_file(path: Union[str, Path]) -> dict[str, Any]:
40
- """Loads the raw dict from a YAML file.
41
-
42
- Args:
43
- path: The path to the agent.yaml file.
44
-
45
- Returns:
46
- dict: The raw dictionary content.
47
-
48
- Raises:
49
- ManifestSyntaxError: If YAML is invalid.
50
- FileNotFoundError: If the file does not exist.
51
- """
52
- try:
53
- path_obj = Path(path)
54
- if not path_obj.exists():
55
- raise FileNotFoundError(f"Manifest file not found: {path}")
56
-
57
- with open(path_obj, "r", encoding="utf-8") as f:
58
- # safe_load is recommended for untrusted input
59
- data = yaml.safe_load(f)
60
-
61
- if not isinstance(data, dict):
62
- raise ManifestSyntaxError(f"Invalid YAML content in {path}: must be a dictionary.")
63
-
64
- ManifestLoader._normalize_data(data)
65
-
66
- return data
67
-
68
- except yaml.YAMLError as e:
69
- raise ManifestSyntaxError(f"Failed to parse YAML file {path}: {str(e)}") from e
70
- except OSError as e:
71
- if isinstance(e, FileNotFoundError):
72
- raise
73
- raise ManifestSyntaxError(f"Error reading file {path}: {str(e)}") from e
74
-
75
- @staticmethod
76
- async def load_raw_from_file_async(path: Union[str, Path]) -> dict[str, Any]:
77
- """Loads the raw dict from a YAML file asynchronously.
78
-
79
- Args:
80
- path: The path to the agent.yaml file.
81
-
82
- Returns:
83
- dict: The raw dictionary content.
84
-
85
- Raises:
86
- ManifestSyntaxError: If YAML is invalid.
87
- FileNotFoundError: If the file does not exist.
88
- """
89
- try:
90
- path_obj = Path(path)
91
- if not path_obj.exists():
92
- raise FileNotFoundError(f"Manifest file not found: {path}")
93
-
94
- async with aiofiles.open(path_obj, "r", encoding="utf-8") as f:
95
- content = await f.read()
96
- data = yaml.safe_load(content)
97
-
98
- if not isinstance(data, dict):
99
- raise ManifestSyntaxError(f"Invalid YAML content in {path}: must be a dictionary.")
100
-
101
- ManifestLoader._normalize_data(data)
102
-
103
- return data
104
-
105
- except yaml.YAMLError as e:
106
- raise ManifestSyntaxError(f"Failed to parse YAML file {path}: {str(e)}") from e
107
- except OSError as e:
108
- if isinstance(e, FileNotFoundError):
109
- raise
110
- raise ManifestSyntaxError(f"Error reading file {path}: {str(e)}") from e
111
-
112
- @staticmethod
113
- def load_from_file(path: Union[str, Path]) -> AgentDefinition:
114
- """Loads the agent manifest from a YAML file.
115
-
116
- Args:
117
- path: The path to the agent.yaml file.
118
-
119
- Returns:
120
- AgentDefinition: The validated Pydantic model.
121
-
122
- Raises:
123
- ManifestSyntaxError: If YAML is invalid or Pydantic validation fails.
124
- FileNotFoundError: If the file does not exist.
125
- """
126
- data = ManifestLoader.load_raw_from_file(path)
127
- return ManifestLoader.load_from_dict(data)
128
-
129
- @staticmethod
130
- def load_from_dict(data: dict[str, Any]) -> AgentDefinition:
131
- """Converts a dictionary into an AgentDefinition model.
132
-
133
- Args:
134
- data: The raw dictionary.
135
-
136
- Returns:
137
- AgentDefinition: The validated Pydantic model.
138
-
139
- Raises:
140
- ManifestSyntaxError: If Pydantic validation fails.
141
- """
142
- try:
143
- # Ensure normalization happens before Pydantic validation
144
- # We work on a copy to avoid side effects if possible, but deep copy is expensive.
145
- # The input 'data' might be modified in place.
146
- ManifestLoader._normalize_data(data)
147
-
148
- return AgentDefinition.model_validate(data)
149
- except ValidationError as e:
150
- # Convert Pydantic ValidationError to ManifestSyntaxError
151
- # We assume "normalization" happens via Pydantic validators (e.g. UUID, SemVer checks)
152
- raise ManifestSyntaxError(f"Manifest validation failed: {str(e)}") from e
153
-
154
- @staticmethod
155
- def _normalize_data(data: dict[str, Any]) -> None:
156
- """Normalizes the data dictionary in place.
157
-
158
- Specifically strips 'v' or 'V' from version strings recursively until clean.
159
- """
160
- if "metadata" in data and isinstance(data["metadata"], dict):
161
- version = data["metadata"].get("version")
162
- if isinstance(version, str):
163
- # Recursively strip leading 'v' or 'V'
164
- while version and version[0] in ("v", "V"):
165
- version = version[1:]
166
- data["metadata"]["version"] = version
167
-
168
- @staticmethod
169
- def inspect_function(func: Callable[..., Any]) -> AgentInterface:
170
- """Generates an AgentInterface from a Python function.
171
-
172
- Scans the function signature. If `user_context` (by name) or UserContext (by type)
173
- is found, it is marked as injected and excluded from the public schema.
174
-
175
- Args:
176
- func: The function to inspect.
177
-
178
- Returns:
179
- AgentInterface: The generated interface definition.
180
-
181
- Raises:
182
- ManifestSyntaxError: If forbidden arguments are found.
183
- """
184
- sig = inspect.signature(func)
185
- try:
186
- type_hints = get_type_hints(func)
187
- except Exception:
188
- # Fallback if get_type_hints fails (e.g. forward refs issues)
189
- type_hints = {}
190
-
191
- field_definitions: Dict[str, Any] = {}
192
- injected: List[str] = []
193
-
194
- for param_name, param in sig.parameters.items():
195
- if param_name in ("self", "cls"):
196
- continue
197
-
198
- # Determine type annotation
199
- annotation = type_hints.get(param_name, param.annotation)
200
- if annotation is inspect.Parameter.empty:
201
- annotation = Any
202
-
203
- # Check for forbidden arguments
204
- if param_name in ("api_key", "token"):
205
- raise ManifestSyntaxError(f"Function argument '{param_name}' is forbidden. Use UserContext for auth.")
206
-
207
- # Check for injection
208
- is_injected = False
209
- if param_name == "user_context":
210
- is_injected = True
211
- else:
212
- # Check direct type
213
- if annotation is UserContext:
214
- is_injected = True
215
- else:
216
- # Check for Optional[UserContext], Annotated[UserContext, ...], Union[UserContext, ...]
217
- origin = get_origin(annotation)
218
- args = get_args(annotation)
219
- if origin is not None:
220
- # Recursively check if UserContext is in args (handles Optional/Union)
221
- # or if this is Annotated (UserContext might be the first arg)
222
- # We do a shallow check on args.
223
- for arg in args:
224
- if arg is UserContext:
225
- is_injected = True
226
- break
227
-
228
- if is_injected:
229
- if "user_context" not in injected:
230
- injected.append("user_context")
231
- continue
232
-
233
- # Prepare for Pydantic model creation
234
- default = param.default
235
- if default is inspect.Parameter.empty:
236
- default = ...
237
-
238
- field_definitions[param_name] = (annotation, default)
239
-
240
- # Create dynamic model to generate JSON Schema for inputs
241
- # We assume strict mode or similar is handled by the consumer, here we just describe it.
242
- try:
243
- InputsModel = create_model("Inputs", **field_definitions)
244
- inputs_schema = InputsModel.model_json_schema()
245
- except Exception as e:
246
- raise ManifestSyntaxError(f"Failed to generate schema from function signature: {e}") from e
247
-
248
- # Handle return type for outputs
249
- return_annotation = type_hints.get("return", sig.return_annotation)
250
- outputs_schema = {}
251
- if (
252
- return_annotation is not inspect.Parameter.empty
253
- and return_annotation is not None
254
- and return_annotation is not type(None)
255
- ):
256
- try:
257
- # If return annotation is a Pydantic model, use its schema
258
- if hasattr(return_annotation, "model_json_schema"):
259
- outputs_schema = return_annotation.model_json_schema()
260
- else:
261
- # Wrap in a model
262
- OutputsModel = create_model("Outputs", result=(return_annotation, ...))
263
- outputs_schema = OutputsModel.model_json_schema()
264
- except Exception:
265
- pass
266
-
267
- return AgentInterface(
268
- inputs=inputs_schema,
269
- outputs=outputs_schema,
270
- injected_params=injected,
271
- )
coreason_manifest/main.py DELETED
@@ -1,17 +0,0 @@
1
- # Prosperity-3.0
2
- """Main module for coreason_manifest.
3
-
4
- This module is primarily used for testing and demonstration purposes.
5
- """
6
-
7
- from coreason_manifest.utils.logger import logger
8
-
9
-
10
- def hello_world() -> str:
11
- """Returns a hello world string.
12
-
13
- Returns:
14
- "Hello World!" string.
15
- """
16
- logger.info("Hello World!")
17
- return "Hello World!"
@@ -1,81 +0,0 @@
1
- package coreason.compliance
2
-
3
- import rego.v1
4
-
5
- # Do not import data.tbom to avoid namespace confusion, access via data.tbom directly if needed or via helper.
6
-
7
- default allow := false
8
-
9
- # Deny if 'pickle' is in libraries (matches "pickle", "pickle==1.0", "pickle>=2.0")
10
- deny contains msg if {
11
- some i
12
- lib_str := input.dependencies.libraries[i]
13
- # Check if the library name starts with 'pickle' followed by end of string or version specifier
14
- regex.match("^pickle([<>=!@\\[].*)?$", lib_str)
15
- msg := "Security Risk: 'pickle' library is strictly forbidden."
16
- }
17
-
18
- # Deny if 'os' is in libraries
19
- deny contains msg if {
20
- some i
21
- lib_str := input.dependencies.libraries[i]
22
- regex.match("^os([<>=!@\\[].*)?$", lib_str)
23
- msg := "Security Risk: 'os' library is strictly forbidden."
24
- }
25
-
26
- # Deny if description is too short (Business Rule example)
27
- # Iterates over ALL steps to ensure compliance.
28
- deny contains msg if {
29
- some step in input.topology.steps
30
- count(step.description) < 5
31
- msg := "Step description is too short."
32
- }
33
-
34
- # Rule 1 (Dependency Pinning): All library dependencies must have explicit version pins.
35
- # Strictly enforces "name==version" (with optional extras).
36
- # Rejects "name>=version", "name==version,>=other", etc.
37
- deny contains msg if {
38
- some i
39
- lib := input.dependencies.libraries[i]
40
-
41
- # Regex Explanation:
42
- # ^ Start
43
- # [a-zA-Z0-9_\-\.]+ Package name (alphanum, _, -, .)
44
- # (\[[a-zA-Z0-9_\-\.,]+\])? Optional extras in brackets (e.g. [security,fast])
45
- # == Must be strictly '=='
46
- # [a-zA-Z0-9_\-\.\+]+ Version string (alphanum, _, -, ., + for metadata)
47
- # $ End (No trailing constraints like ,>=2.0)
48
-
49
- not regex.match("^[a-zA-Z0-9_\\-\\.]+(\\[[a-zA-Z0-9_\\-\\.,]+\\])?==[a-zA-Z0-9_\\-\\.\\+]+$", lib)
50
- msg := sprintf("Compliance Violation: Library '%v' must be strictly pinned with '==' (e.g., 'pandas==2.0.1').", [lib])
51
- }
52
-
53
- # Rule 2 (Allowlist Enforcement): Libraries must be in TBOM
54
- deny contains msg if {
55
- some i
56
- lib_str := input.dependencies.libraries[i]
57
-
58
- # Extract library name using regex
59
- # Pattern must support dots (for namespace packages) and stop before extras brackets or version specifiers.
60
- parts := regex.find_all_string_submatch_n("^[a-zA-Z0-9_\\-\\.]+", lib_str, 1)
61
- count(parts) > 0
62
- lib_name := parts[0][0]
63
-
64
- # Check if lib_name is in tbom (case-insensitive)
65
- not is_in_tbom(lib_name)
66
-
67
- msg := sprintf("Compliance Violation: Library '%v' is not in the Trusted Bill of Materials (TBOM).", [lib_name])
68
- }
69
-
70
- # Helper to safely check if lib is in TBOM
71
- # Returns true if data.tbom exists AND contains the lib (case-insensitive)
72
- is_in_tbom(lib) if {
73
- # Lowercase the input library name
74
- lower_lib := lower(lib)
75
-
76
- # Check against TBOM
77
- # If data.tbom is undefined, this rule body is undefined (false).
78
- # Iterate through TBOM and compare lowercased versions
79
- some tbom_lib in data.tbom
80
- lower(tbom_lib) == lower_lib
81
- }
@@ -1,14 +0,0 @@
1
- {
2
- "tbom": [
3
- "requests",
4
- "pydantic",
5
- "httpx",
6
- "pandas",
7
- "numpy",
8
- "scipy",
9
- "scikit-learn",
10
- "torch",
11
- "fastapi",
12
- "uvicorn"
13
- ]
14
- }
@@ -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}