coreason-manifest 0.9.0__py3-none-any.whl → 0.12.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.
- coreason_manifest/__init__.py +47 -13
- coreason_manifest/common.py +64 -0
- coreason_manifest/governance.py +83 -0
- coreason_manifest/schemas/__init__.py +9 -1
- coreason_manifest/schemas/coreason-v2.schema.json +462 -0
- coreason_manifest/utils/__init__.py +10 -0
- coreason_manifest/utils/logger.py +10 -0
- coreason_manifest/v2/__init__.py +1 -0
- coreason_manifest/v2/governance.py +144 -0
- coreason_manifest/v2/io.py +132 -0
- coreason_manifest/v2/resolver.py +67 -0
- coreason_manifest/v2/spec/__init__.py +1 -0
- coreason_manifest/v2/spec/contracts.py +34 -0
- coreason_manifest/v2/spec/definitions.py +196 -0
- coreason_manifest/v2/validator.py +48 -0
- {coreason_manifest-0.9.0.dist-info → coreason_manifest-0.12.0.dist-info}/METADATA +68 -29
- coreason_manifest-0.12.0.dist-info/RECORD +20 -0
- {coreason_manifest-0.9.0.dist-info → coreason_manifest-0.12.0.dist-info}/WHEEL +1 -1
- coreason_manifest/definitions/__init__.py +0 -49
- coreason_manifest/definitions/agent.py +0 -292
- coreason_manifest/definitions/audit.py +0 -122
- coreason_manifest/definitions/events.py +0 -392
- coreason_manifest/definitions/message.py +0 -126
- coreason_manifest/definitions/simulation.py +0 -50
- coreason_manifest/definitions/topology.py +0 -255
- coreason_manifest/recipes.py +0 -76
- coreason_manifest/schemas/agent.schema.json +0 -849
- coreason_manifest-0.9.0.dist-info/RECORD +0 -18
- {coreason_manifest-0.9.0.dist-info → coreason_manifest-0.12.0.dist-info}/licenses/LICENSE +0 -0
- {coreason_manifest-0.9.0.dist-info → coreason_manifest-0.12.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -1,3 +1,13 @@
|
|
|
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-manifest
|
|
10
|
+
|
|
1
11
|
# Copyright (c) 2025 CoReason, Inc.
|
|
2
12
|
#
|
|
3
13
|
# This software is proprietary and dual-licensed.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Coreason Manifest V2
|
|
@@ -0,0 +1,144 @@
|
|
|
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-manifest
|
|
10
|
+
|
|
11
|
+
"""Governance logic for V2 Manifests."""
|
|
12
|
+
|
|
13
|
+
from typing import List
|
|
14
|
+
from urllib.parse import urlparse
|
|
15
|
+
|
|
16
|
+
from coreason_manifest.common import ToolRiskLevel
|
|
17
|
+
from coreason_manifest.governance import (
|
|
18
|
+
ComplianceReport,
|
|
19
|
+
ComplianceViolation,
|
|
20
|
+
GovernanceConfig,
|
|
21
|
+
)
|
|
22
|
+
from coreason_manifest.v2.spec.definitions import (
|
|
23
|
+
LogicStep,
|
|
24
|
+
ManifestV2,
|
|
25
|
+
SwitchStep,
|
|
26
|
+
ToolDefinition,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _risk_score(level: ToolRiskLevel) -> int:
|
|
31
|
+
"""Convert risk level to integer score for comparison."""
|
|
32
|
+
if level == ToolRiskLevel.SAFE:
|
|
33
|
+
return 0
|
|
34
|
+
if level == ToolRiskLevel.STANDARD:
|
|
35
|
+
return 1
|
|
36
|
+
if level == ToolRiskLevel.CRITICAL:
|
|
37
|
+
return 2
|
|
38
|
+
return 3 # Unknown is highest risk
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def check_compliance_v2(manifest: ManifestV2, config: GovernanceConfig) -> ComplianceReport:
|
|
42
|
+
"""Enforce policy on V2 Manifest before compilation.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
manifest: The V2 manifest to check.
|
|
46
|
+
config: The governance policy configuration.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
A ComplianceReport detailing violations.
|
|
50
|
+
"""
|
|
51
|
+
violations: List[ComplianceViolation] = []
|
|
52
|
+
|
|
53
|
+
# 1. Check Tools in Definitions
|
|
54
|
+
for _, definition in manifest.definitions.items():
|
|
55
|
+
if isinstance(definition, ToolDefinition):
|
|
56
|
+
# Check Risk Level
|
|
57
|
+
if config.max_risk_level:
|
|
58
|
+
tool_score = _risk_score(definition.risk_level)
|
|
59
|
+
max_score = _risk_score(config.max_risk_level)
|
|
60
|
+
if tool_score > max_score:
|
|
61
|
+
violations.append(
|
|
62
|
+
ComplianceViolation(
|
|
63
|
+
rule="risk_level_restriction",
|
|
64
|
+
message=(
|
|
65
|
+
f"Tool '{definition.name}' risk level '{definition.risk_level.value}' "
|
|
66
|
+
f"exceeds allowed maximum '{config.max_risk_level.value}'."
|
|
67
|
+
),
|
|
68
|
+
component_id=definition.id,
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Check Allowed Domains
|
|
73
|
+
if config.allowed_domains is not None:
|
|
74
|
+
try:
|
|
75
|
+
parsed_uri = urlparse(str(definition.uri))
|
|
76
|
+
hostname = parsed_uri.hostname
|
|
77
|
+
|
|
78
|
+
if not hostname:
|
|
79
|
+
violations.append(
|
|
80
|
+
ComplianceViolation(
|
|
81
|
+
rule="domain_restriction",
|
|
82
|
+
message=(f"Tool '{definition.name}' URI '{definition.uri}' has no hostname."),
|
|
83
|
+
component_id=definition.id,
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
else:
|
|
87
|
+
# Normalize hostname
|
|
88
|
+
hostname = hostname.lower()
|
|
89
|
+
if hostname.endswith("."):
|
|
90
|
+
hostname = hostname[:-1]
|
|
91
|
+
|
|
92
|
+
# Prepare allowed set
|
|
93
|
+
if config.strict_url_validation:
|
|
94
|
+
allowed_set = {d.lower() for d in config.allowed_domains if d}
|
|
95
|
+
else:
|
|
96
|
+
allowed_set = set(config.allowed_domains)
|
|
97
|
+
|
|
98
|
+
if hostname not in allowed_set:
|
|
99
|
+
violations.append(
|
|
100
|
+
ComplianceViolation(
|
|
101
|
+
rule="domain_restriction",
|
|
102
|
+
message=(
|
|
103
|
+
f"Tool '{definition.name}' URI host '{hostname}' "
|
|
104
|
+
f"is not in allowed domains: {config.allowed_domains}"
|
|
105
|
+
),
|
|
106
|
+
component_id=definition.id,
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
except Exception as e:
|
|
110
|
+
violations.append(
|
|
111
|
+
ComplianceViolation(
|
|
112
|
+
rule="domain_restriction",
|
|
113
|
+
message=f"Failed to parse tool URI '{definition.uri}': {e}",
|
|
114
|
+
component_id=definition.id,
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# 2. Check Workflow Steps for Custom Logic
|
|
119
|
+
if not config.allow_custom_logic:
|
|
120
|
+
for step_id, step in manifest.workflow.steps.items():
|
|
121
|
+
if isinstance(step, LogicStep):
|
|
122
|
+
violations.append(
|
|
123
|
+
ComplianceViolation(
|
|
124
|
+
rule="custom_logic_restriction",
|
|
125
|
+
message="LogicStep containing custom code is not allowed by policy.",
|
|
126
|
+
component_id=step_id,
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
elif isinstance(step, SwitchStep):
|
|
130
|
+
# Flag complex conditions (function calls, imports, internals) as custom logic.
|
|
131
|
+
for condition in step.cases.keys():
|
|
132
|
+
if "(" in condition or "import " in condition or "__" in condition:
|
|
133
|
+
violations.append(
|
|
134
|
+
ComplianceViolation(
|
|
135
|
+
rule="custom_logic_restriction",
|
|
136
|
+
message=(
|
|
137
|
+
f"SwitchStep '{step_id}' contains complex condition '{condition}' "
|
|
138
|
+
"which is flagged as custom logic."
|
|
139
|
+
),
|
|
140
|
+
component_id=step_id,
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return ComplianceReport(passed=len(violations) == 0, violations=violations)
|
|
@@ -0,0 +1,132 @@
|
|
|
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-manifest
|
|
10
|
+
|
|
11
|
+
"""I/O module for loading and dumping V2 Manifests."""
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, Optional, Set, Union
|
|
15
|
+
|
|
16
|
+
import yaml
|
|
17
|
+
|
|
18
|
+
from coreason_manifest.v2.resolver import ReferenceResolver
|
|
19
|
+
from coreason_manifest.v2.spec.definitions import ManifestV2
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _load_recursive(path: Path, resolver: ReferenceResolver, visited_paths: Set[Path]) -> Dict[str, Any]:
|
|
23
|
+
"""
|
|
24
|
+
Recursively load YAML data, resolving $ref in definitions.
|
|
25
|
+
|
|
26
|
+
Handles cycle detection to prevent infinite recursion and uses the
|
|
27
|
+
ReferenceResolver to ensure secure path resolution.
|
|
28
|
+
"""
|
|
29
|
+
if path in visited_paths:
|
|
30
|
+
raise RecursionError(f"Circular dependency detected: {path}")
|
|
31
|
+
|
|
32
|
+
visited_paths.add(path)
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
with path.open("r", encoding="utf-8") as f:
|
|
36
|
+
data = yaml.safe_load(f)
|
|
37
|
+
except yaml.YAMLError as e:
|
|
38
|
+
raise ValueError(f"Invalid YAML in {path}: {e}") from e
|
|
39
|
+
|
|
40
|
+
if not isinstance(data, dict):
|
|
41
|
+
raise ValueError(f"Expected a dictionary in {path}, got {type(data).__name__}")
|
|
42
|
+
|
|
43
|
+
# Resolve references in definitions
|
|
44
|
+
definitions = data.get("definitions", {})
|
|
45
|
+
if definitions:
|
|
46
|
+
for key, value in definitions.items():
|
|
47
|
+
if isinstance(value, dict) and "$ref" in value:
|
|
48
|
+
ref_path_str = value["$ref"]
|
|
49
|
+
# Resolve the path
|
|
50
|
+
abs_path = resolver.resolve(path, ref_path_str)
|
|
51
|
+
|
|
52
|
+
# Recursively load
|
|
53
|
+
loaded_obj = _load_recursive(abs_path, resolver, visited_paths)
|
|
54
|
+
|
|
55
|
+
# Merge Strategy:
|
|
56
|
+
# Replace the $ref dict with the loaded data.
|
|
57
|
+
definitions[key] = loaded_obj
|
|
58
|
+
|
|
59
|
+
visited_paths.remove(path)
|
|
60
|
+
return data
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def load_from_yaml(
|
|
64
|
+
path: Union[str, Path],
|
|
65
|
+
root_dir: Optional[Union[str, Path]] = None,
|
|
66
|
+
recursive: bool = True,
|
|
67
|
+
) -> ManifestV2:
|
|
68
|
+
"""Load a V2 manifest from a YAML file.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
path: Path to the YAML file.
|
|
72
|
+
root_dir: The allowed root directory for references. Defaults to path's parent.
|
|
73
|
+
recursive: Whether to resolve $ref recursively.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
The validated ManifestV2 object.
|
|
77
|
+
|
|
78
|
+
Raises:
|
|
79
|
+
FileNotFoundError: If the file does not exist.
|
|
80
|
+
ValidationError: If the manifest is invalid.
|
|
81
|
+
yaml.YAMLError: If the YAML is invalid.
|
|
82
|
+
RecursionError: If a cyclic dependency is detected.
|
|
83
|
+
ValueError: If a security violation occurs or YAML is invalid.
|
|
84
|
+
"""
|
|
85
|
+
p = Path(path).resolve()
|
|
86
|
+
if not p.exists():
|
|
87
|
+
raise FileNotFoundError(f"Manifest file not found: {path}")
|
|
88
|
+
|
|
89
|
+
if root_dir is None:
|
|
90
|
+
root_dir = p.parent
|
|
91
|
+
|
|
92
|
+
if recursive:
|
|
93
|
+
resolver = ReferenceResolver(root_dir)
|
|
94
|
+
visited_paths: Set[Path] = set()
|
|
95
|
+
data = _load_recursive(p, resolver, visited_paths)
|
|
96
|
+
else:
|
|
97
|
+
with p.open("r", encoding="utf-8") as f:
|
|
98
|
+
data = yaml.safe_load(f)
|
|
99
|
+
|
|
100
|
+
return ManifestV2.model_validate(data)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def dump_to_yaml(manifest: ManifestV2) -> str:
|
|
104
|
+
"""Dump a V2 manifest to a YAML string.
|
|
105
|
+
|
|
106
|
+
Ensures that apiVersion, kind, and metadata appear at the top.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
manifest: The ManifestV2 object to dump.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
The YAML string representation.
|
|
113
|
+
"""
|
|
114
|
+
# Serialize to dict, using aliases (e.g., x-design) and excluding None
|
|
115
|
+
data = manifest.model_dump(by_alias=True, exclude_none=True)
|
|
116
|
+
|
|
117
|
+
# Reorder keys to ensure human readability
|
|
118
|
+
# Priority keys: apiVersion, kind, metadata
|
|
119
|
+
ordered_keys = ["apiVersion", "kind", "metadata"]
|
|
120
|
+
ordered_data = {}
|
|
121
|
+
|
|
122
|
+
# 1. Add priority keys
|
|
123
|
+
for key in ordered_keys:
|
|
124
|
+
if key in data:
|
|
125
|
+
ordered_data[key] = data.pop(key)
|
|
126
|
+
|
|
127
|
+
# 2. Add remaining keys
|
|
128
|
+
ordered_data.update(data)
|
|
129
|
+
|
|
130
|
+
# Dump using PyYAML, preserving key order (sort_keys=False)
|
|
131
|
+
# allow_unicode=True ensures proper string representation
|
|
132
|
+
return yaml.dump(ordered_data, sort_keys=False, allow_unicode=True)
|
|
@@ -0,0 +1,67 @@
|
|
|
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-manifest
|
|
10
|
+
|
|
11
|
+
"""Resolver module for secure file reference handling."""
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Union
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ReferenceResolver:
|
|
18
|
+
"""
|
|
19
|
+
Resolves file references with security constraints (Jail).
|
|
20
|
+
|
|
21
|
+
This class enforces strict security boundaries to prevent Path Traversal attacks.
|
|
22
|
+
All resolved paths must be contained within the specified root directory.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, root_dir: Union[str, Path]):
|
|
26
|
+
"""
|
|
27
|
+
Initialize the resolver with a root directory.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
root_dir: The allowed jail directory. All resolved paths must be within this directory.
|
|
31
|
+
"""
|
|
32
|
+
self.root_dir = Path(root_dir).resolve()
|
|
33
|
+
|
|
34
|
+
def resolve(self, base_file: Path, ref_path: str) -> Path:
|
|
35
|
+
"""
|
|
36
|
+
Resolve a reference relative to a base file, ensuring it stays within the root directory.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
base_file: The file containing the reference.
|
|
40
|
+
ref_path: The relative path string to resolve.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
The absolute resolved Path.
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
ValueError: If the resolved path escapes the root directory.
|
|
47
|
+
FileNotFoundError: If the referenced file does not exist.
|
|
48
|
+
"""
|
|
49
|
+
# Ensure base_file is absolute
|
|
50
|
+
base_file = base_file.resolve()
|
|
51
|
+
|
|
52
|
+
# Combine base_file parent with ref_path
|
|
53
|
+
# We assume ref_path is relative to the base_file's location
|
|
54
|
+
target_path = (base_file.parent / ref_path).resolve()
|
|
55
|
+
|
|
56
|
+
# Security Check: Jail
|
|
57
|
+
try:
|
|
58
|
+
target_path.relative_to(self.root_dir)
|
|
59
|
+
except ValueError:
|
|
60
|
+
raise ValueError(
|
|
61
|
+
f"Security Error: Reference '{ref_path}' escapes the root directory '{self.root_dir}'."
|
|
62
|
+
) from None
|
|
63
|
+
|
|
64
|
+
if not target_path.exists():
|
|
65
|
+
raise FileNotFoundError(f"Referenced file not found: {target_path}")
|
|
66
|
+
|
|
67
|
+
return target_path
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Coreason Manifest V2 Spec Definitions
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class InterfaceDefinition(BaseModel):
|
|
7
|
+
"""Defines the input/output contract."""
|
|
8
|
+
|
|
9
|
+
model_config = ConfigDict(extra="forbid")
|
|
10
|
+
|
|
11
|
+
inputs: Dict[str, Any] = Field(default_factory=dict, description="JSON Schema definitions for arguments.")
|
|
12
|
+
outputs: Dict[str, Any] = Field(default_factory=dict, description="JSON Schema definitions for return values.")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class StateDefinition(BaseModel):
|
|
16
|
+
"""Defines the conversation memory/context structure."""
|
|
17
|
+
|
|
18
|
+
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
19
|
+
|
|
20
|
+
schema_: Dict[str, Any] = Field(
|
|
21
|
+
default_factory=dict, alias="schema", description="The structure of the conversation memory/context."
|
|
22
|
+
)
|
|
23
|
+
backend: Optional[str] = Field(None, description="Backend storage type (e.g., 'redis', 'memory').")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class PolicyDefinition(BaseModel):
|
|
27
|
+
"""Defines execution policy and governance rules."""
|
|
28
|
+
|
|
29
|
+
model_config = ConfigDict(extra="forbid")
|
|
30
|
+
|
|
31
|
+
max_steps: Optional[int] = Field(None, description="Execution limit on number of steps.")
|
|
32
|
+
max_retries: int = Field(3, description="Maximum number of retries.")
|
|
33
|
+
timeout: Optional[int] = Field(None, description="Timeout in seconds.")
|
|
34
|
+
human_in_the_loop: bool = Field(False, description="Whether to require human approval.")
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
from typing import Annotated, Any, Dict, List, Literal, Optional, Union
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
4
|
+
|
|
5
|
+
from coreason_manifest.common import StrictUri, ToolRiskLevel
|
|
6
|
+
from coreason_manifest.v2.spec.contracts import InterfaceDefinition, PolicyDefinition, StateDefinition
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DesignMetadata(BaseModel):
|
|
10
|
+
"""UI-specific metadata for the visual builder."""
|
|
11
|
+
|
|
12
|
+
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
13
|
+
|
|
14
|
+
x: float = Field(..., description="X coordinate on the canvas.")
|
|
15
|
+
y: float = Field(..., description="Y coordinate on the canvas.")
|
|
16
|
+
icon: Optional[str] = Field(None, description="Icon name or URL.")
|
|
17
|
+
color: Optional[str] = Field(None, description="Color code (hex/name).")
|
|
18
|
+
label: Optional[str] = Field(None, description="Display label.")
|
|
19
|
+
zoom: Optional[float] = Field(None, description="Zoom level.")
|
|
20
|
+
collapsed: bool = Field(False, description="Whether the node is collapsed in UI.")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ToolDefinition(BaseModel):
|
|
24
|
+
"""Definition of an external tool."""
|
|
25
|
+
|
|
26
|
+
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
27
|
+
|
|
28
|
+
type: Literal["tool"] = "tool"
|
|
29
|
+
id: str = Field(..., description="Unique ID for the tool within the manifest.")
|
|
30
|
+
name: str = Field(..., description="Name of the tool.")
|
|
31
|
+
uri: StrictUri = Field(..., description="The MCP endpoint URI.")
|
|
32
|
+
risk_level: ToolRiskLevel = Field(..., description="Risk level (safe, standard, critical).")
|
|
33
|
+
description: Optional[str] = Field(None, description="Description of the tool.")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AgentDefinition(BaseModel):
|
|
37
|
+
"""Definition of an Agent."""
|
|
38
|
+
|
|
39
|
+
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
40
|
+
|
|
41
|
+
type: Literal["agent"] = "agent"
|
|
42
|
+
id: str = Field(..., description="Unique ID for the agent.")
|
|
43
|
+
name: str = Field(..., description="Name of the agent.")
|
|
44
|
+
role: str = Field(..., description="The persona/job title.")
|
|
45
|
+
goal: str = Field(..., description="Primary objective.")
|
|
46
|
+
backstory: Optional[str] = Field(None, description="Backstory or directives.")
|
|
47
|
+
model: Optional[str] = Field(None, description="LLM identifier.")
|
|
48
|
+
tools: List[str] = Field(default_factory=list, description="List of Tool IDs or URI references.")
|
|
49
|
+
knowledge: List[str] = Field(default_factory=list, description="List of file paths or knowledge base IDs.")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class GenericDefinition(BaseModel):
|
|
53
|
+
"""Fallback for unknown definitions."""
|
|
54
|
+
|
|
55
|
+
model_config = ConfigDict(extra="allow")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class BaseStep(BaseModel):
|
|
59
|
+
"""Base attributes for all steps."""
|
|
60
|
+
|
|
61
|
+
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
62
|
+
|
|
63
|
+
id: str = Field(..., description="Unique identifier for the step.")
|
|
64
|
+
inputs: Dict[str, Any] = Field(default_factory=dict, description="Input arguments for the step.")
|
|
65
|
+
design_metadata: Optional[DesignMetadata] = Field(None, alias="x-design", description="UI metadata.")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class AgentStep(BaseStep):
|
|
69
|
+
"""A step that executes an AI Agent."""
|
|
70
|
+
|
|
71
|
+
type: Literal["agent"] = "agent"
|
|
72
|
+
agent: str = Field(..., description="Reference to an Agent definition (by ID or name).")
|
|
73
|
+
next: Optional[str] = Field(None, description="ID of the next step to execute.")
|
|
74
|
+
system_prompt: Optional[str] = Field(None, description="Optional override for system prompt.")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class LogicStep(BaseStep):
|
|
78
|
+
"""A step that executes custom logic."""
|
|
79
|
+
|
|
80
|
+
type: Literal["logic"] = "logic"
|
|
81
|
+
code: str = Field(..., description="Python code or reference to logic to execute.")
|
|
82
|
+
next: Optional[str] = Field(None, description="ID of the next step to execute.")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class CouncilStep(BaseStep):
|
|
86
|
+
"""A step that involves multiple voters/agents."""
|
|
87
|
+
|
|
88
|
+
type: Literal["council"] = "council"
|
|
89
|
+
voters: List[str] = Field(..., description="List of voters (Agent IDs).")
|
|
90
|
+
strategy: str = Field("consensus", description="Voting strategy (e.g., consensus, majority).")
|
|
91
|
+
next: Optional[str] = Field(None, description="ID of the next step to execute.")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class SwitchStep(BaseStep):
|
|
95
|
+
"""A step that routes execution based on conditions."""
|
|
96
|
+
|
|
97
|
+
type: Literal["switch"] = "switch"
|
|
98
|
+
cases: Dict[str, str] = Field(..., description="Dictionary of condition expressions to Step IDs.")
|
|
99
|
+
default: Optional[str] = Field(None, description="Default Step ID if no cases match.")
|
|
100
|
+
# Note: 'next' is deliberately excluded for SwitchStep in favor of cases/default.
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
Step = Annotated[
|
|
104
|
+
Union[AgentStep, LogicStep, CouncilStep, SwitchStep],
|
|
105
|
+
Field(discriminator="type", description="Polymorphic step definition."),
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class Workflow(BaseModel):
|
|
110
|
+
"""Defines the execution topology."""
|
|
111
|
+
|
|
112
|
+
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
113
|
+
|
|
114
|
+
start: str = Field(..., description="ID of the starting step.")
|
|
115
|
+
steps: Dict[str, Step] = Field(..., description="Dictionary of all steps indexed by ID.")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class ManifestMetadata(BaseModel):
|
|
119
|
+
"""Metadata for the manifest."""
|
|
120
|
+
|
|
121
|
+
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
122
|
+
|
|
123
|
+
name: str = Field(..., description="Human-readable name of the workflow/agent.")
|
|
124
|
+
design_metadata: Optional[DesignMetadata] = Field(None, alias="x-design", description="UI metadata.")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class ManifestV2(BaseModel):
|
|
128
|
+
"""Root object for Coreason Manifest V2."""
|
|
129
|
+
|
|
130
|
+
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
131
|
+
|
|
132
|
+
apiVersion: Literal["coreason.ai/v2"] = Field("coreason.ai/v2", description="API Version.")
|
|
133
|
+
kind: Literal["Recipe", "Agent"] = Field(..., description="Kind of the object.")
|
|
134
|
+
metadata: ManifestMetadata = Field(..., description="Metadata including name and design info.")
|
|
135
|
+
interface: InterfaceDefinition = Field(default_factory=InterfaceDefinition)
|
|
136
|
+
state: StateDefinition = Field(default_factory=StateDefinition)
|
|
137
|
+
policy: PolicyDefinition = Field(default_factory=PolicyDefinition)
|
|
138
|
+
definitions: Dict[
|
|
139
|
+
str,
|
|
140
|
+
Union[
|
|
141
|
+
Annotated[Union[ToolDefinition, AgentDefinition], Field(discriminator="type")],
|
|
142
|
+
GenericDefinition,
|
|
143
|
+
],
|
|
144
|
+
] = Field(default_factory=dict, description="Reusable definitions.")
|
|
145
|
+
workflow: Workflow = Field(..., description="The main workflow topology.")
|
|
146
|
+
|
|
147
|
+
@model_validator(mode="after")
|
|
148
|
+
def validate_integrity(self) -> "ManifestV2":
|
|
149
|
+
"""Validate referential integrity of the manifest."""
|
|
150
|
+
steps = self.workflow.steps
|
|
151
|
+
|
|
152
|
+
# 1. Validate Start Step
|
|
153
|
+
if self.workflow.start not in steps:
|
|
154
|
+
raise ValueError(f"Start step '{self.workflow.start}' not found in steps.")
|
|
155
|
+
|
|
156
|
+
for step in steps.values():
|
|
157
|
+
# 2. Validate 'next' pointers (AgentStep, LogicStep, CouncilStep)
|
|
158
|
+
if hasattr(step, "next") and step.next:
|
|
159
|
+
if step.next not in steps:
|
|
160
|
+
raise ValueError(f"Step '{step.id}' references missing next step '{step.next}'.")
|
|
161
|
+
|
|
162
|
+
# 3. Validate SwitchStep targets
|
|
163
|
+
if isinstance(step, SwitchStep):
|
|
164
|
+
for target in step.cases.values():
|
|
165
|
+
if target not in steps:
|
|
166
|
+
raise ValueError(f"SwitchStep '{step.id}' references missing step '{target}' in cases.")
|
|
167
|
+
if step.default and step.default not in steps:
|
|
168
|
+
raise ValueError(f"SwitchStep '{step.id}' references missing step '{step.default}' in default.")
|
|
169
|
+
|
|
170
|
+
# 4. Validate Definition References
|
|
171
|
+
if isinstance(step, AgentStep):
|
|
172
|
+
if step.agent not in self.definitions:
|
|
173
|
+
raise ValueError(f"AgentStep '{step.id}' references missing agent '{step.agent}'.")
|
|
174
|
+
|
|
175
|
+
# Check type
|
|
176
|
+
agent_def = self.definitions[step.agent]
|
|
177
|
+
if not isinstance(agent_def, AgentDefinition):
|
|
178
|
+
raise ValueError(
|
|
179
|
+
f"AgentStep '{step.id}' references '{step.agent}' which is not an AgentDefinition "
|
|
180
|
+
f"(got {type(agent_def).__name__})."
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
if isinstance(step, CouncilStep):
|
|
184
|
+
for voter in step.voters:
|
|
185
|
+
if voter not in self.definitions:
|
|
186
|
+
raise ValueError(f"CouncilStep '{step.id}' references missing voter '{voter}'.")
|
|
187
|
+
|
|
188
|
+
# Check type
|
|
189
|
+
agent_def = self.definitions[voter]
|
|
190
|
+
if not isinstance(agent_def, AgentDefinition):
|
|
191
|
+
raise ValueError(
|
|
192
|
+
f"CouncilStep '{step.id}' references voter '{voter}' which is not an AgentDefinition "
|
|
193
|
+
f"(got {type(agent_def).__name__})."
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
return self
|
|
@@ -0,0 +1,48 @@
|
|
|
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-manifest
|
|
10
|
+
|
|
11
|
+
"""Validation logic for V2 Manifests."""
|
|
12
|
+
|
|
13
|
+
from typing import List
|
|
14
|
+
|
|
15
|
+
from coreason_manifest.v2.spec.definitions import (
|
|
16
|
+
ManifestV2,
|
|
17
|
+
SwitchStep,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def validate_loose(manifest: ManifestV2) -> List[str]:
|
|
22
|
+
"""Validate "Draft" manifests for structural sanity only.
|
|
23
|
+
|
|
24
|
+
Checks:
|
|
25
|
+
- Unique Step IDs.
|
|
26
|
+
- SwitchStep case syntax (basic).
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
manifest: The V2 manifest to validate.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
List of warning messages (empty if clean).
|
|
33
|
+
"""
|
|
34
|
+
warnings: List[str] = []
|
|
35
|
+
|
|
36
|
+
# 1. Check unique Step IDs
|
|
37
|
+
for step_id, step in manifest.workflow.steps.items():
|
|
38
|
+
if step.id != step_id:
|
|
39
|
+
warnings.append(f"Step key '{step_id}' does not match step.id '{step.id}'.")
|
|
40
|
+
|
|
41
|
+
# 2. Check SwitchStep cases
|
|
42
|
+
for step_id, step in manifest.workflow.steps.items():
|
|
43
|
+
if isinstance(step, SwitchStep):
|
|
44
|
+
for condition in step.cases.keys():
|
|
45
|
+
if not isinstance(condition, str) or not condition.strip():
|
|
46
|
+
warnings.append(f"SwitchStep '{step_id}' has invalid condition: {condition}")
|
|
47
|
+
|
|
48
|
+
return warnings
|