ryeos-engine 0.1.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.
- rye/__init__.py +3 -0
- rye/constants.py +62 -0
- rye/directive_parser.py +158 -0
- rye/executor/__init__.py +19 -0
- rye/executor/chain_validator.py +305 -0
- rye/executor/lockfile_resolver.py +285 -0
- rye/executor/primitive_executor.py +1250 -0
- rye/handlers/__init__.py +13 -0
- rye/handlers/directive/__init__.py +5 -0
- rye/handlers/directive/handler.py +87 -0
- rye/handlers/knowledge/__init__.py +5 -0
- rye/handlers/knowledge/handler.py +89 -0
- rye/handlers/tool/__init__.py +5 -0
- rye/handlers/tool/handler.py +124 -0
- rye/primary_tool_descriptions.py +201 -0
- rye/protocols/__init__.py +1 -0
- rye/protocols/jsonrpc_handler.py +267 -0
- rye/tools/__init__.py +7 -0
- rye/tools/execute.py +388 -0
- rye/tools/load.py +181 -0
- rye/tools/search.py +1168 -0
- rye/tools/sign.py +493 -0
- rye/utils/__init__.py +37 -0
- rye/utils/errors.py +136 -0
- rye/utils/extensions.py +190 -0
- rye/utils/integrity.py +84 -0
- rye/utils/logger.py +147 -0
- rye/utils/metadata_manager.py +443 -0
- rye/utils/parser_router.py +111 -0
- rye/utils/path_utils.py +449 -0
- rye/utils/resolvers.py +190 -0
- rye/utils/signature_formats.py +147 -0
- rye/utils/trust_store.py +251 -0
- rye/utils/validators.py +497 -0
- ryeos_engine-0.1.0.dist-info/METADATA +20 -0
- ryeos_engine-0.1.0.dist-info/RECORD +37 -0
- ryeos_engine-0.1.0.dist-info/WHEEL +4 -0
rye/__init__.py
ADDED
rye/constants.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""RYE Constants
|
|
2
|
+
|
|
3
|
+
Centralized constants for the AI directory name, item types, and tool actions.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
# The name of the working directory used in all three spaces.
|
|
7
|
+
# Every space follows: base_path / AI_DIR / {type_dir} / {item_id}
|
|
8
|
+
AI_DIR = ".ai"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ItemType:
|
|
12
|
+
"""Item type constants."""
|
|
13
|
+
|
|
14
|
+
DIRECTIVE = "directive"
|
|
15
|
+
TOOL = "tool"
|
|
16
|
+
KNOWLEDGE = "knowledge"
|
|
17
|
+
|
|
18
|
+
ALL = [DIRECTIVE, TOOL, KNOWLEDGE]
|
|
19
|
+
|
|
20
|
+
# Type directory mappings
|
|
21
|
+
TYPE_DIRS = {
|
|
22
|
+
DIRECTIVE: "directives",
|
|
23
|
+
TOOL: "tools",
|
|
24
|
+
KNOWLEDGE: "knowledge",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# File extensions to search per item type (tools use dynamic lookup)
|
|
28
|
+
CONTENT_EXTENSIONS = {
|
|
29
|
+
DIRECTIVE: [".md"],
|
|
30
|
+
KNOWLEDGE: [".md", ".yaml", ".yml"],
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Action:
|
|
35
|
+
"""Tool action constants."""
|
|
36
|
+
|
|
37
|
+
SEARCH = "search"
|
|
38
|
+
SIGN = "sign"
|
|
39
|
+
LOAD = "load"
|
|
40
|
+
EXECUTE = "execute"
|
|
41
|
+
|
|
42
|
+
ALL = [SEARCH, SIGN, LOAD, EXECUTE]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Instruction injected into the thread runner's prompt when executing a directive.
|
|
46
|
+
# Used by thread_directive._build_prompt() to instruct the LLM.
|
|
47
|
+
# This is the single most important string for cross-model directive compliance.
|
|
48
|
+
# It must be explicit enough that even weak models follow the body as instructions
|
|
49
|
+
# rather than summarizing, describing, or re-executing.
|
|
50
|
+
DIRECTIVE_INSTRUCTION = (
|
|
51
|
+
"ZERO PREAMBLE. Your very first output token must be directive content — "
|
|
52
|
+
"never narration. Do NOT say 'I need to follow', 'Let me start', "
|
|
53
|
+
"'Here is the output', or ANY framing text.\n\n"
|
|
54
|
+
"You are the executor of this directive. Follow the body step by step.\n\n"
|
|
55
|
+
"<render> → output EXACTLY the text inside. Nothing before, nothing after.\n"
|
|
56
|
+
"<instruction> → follow silently. Do NOT narrate.\n\n"
|
|
57
|
+
"RULES:\n"
|
|
58
|
+
"- Do NOT summarize or describe what you are about to do.\n"
|
|
59
|
+
"- Do NOT re-call execute — you already have the instructions.\n"
|
|
60
|
+
"- If a step says STOP and wait, you MUST stop and wait.\n\n"
|
|
61
|
+
"Begin now with step 1."
|
|
62
|
+
)
|
rye/directive_parser.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Standalone directive parsing: validate inputs, interpolate placeholders.
|
|
2
|
+
|
|
3
|
+
Extracted from ExecuteTool._run_directive() so that both execute.py and
|
|
4
|
+
thread_directive.py can reuse the same logic without circular imports.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
from rye.constants import ItemType
|
|
13
|
+
from rye.utils.integrity import verify_item, IntegrityError
|
|
14
|
+
from rye.utils.parser_router import ParserRouter
|
|
15
|
+
|
|
16
|
+
# {input:key} — required, kept as-is if missing
|
|
17
|
+
# {input:key?} — optional, empty string if missing
|
|
18
|
+
# {input:key:default} — fallback to default if missing (colon separator)
|
|
19
|
+
# {input:key|default} — fallback to default if missing (pipe separator)
|
|
20
|
+
_INPUT_REF = re.compile(r"\{input:(\w+)(\?|[:|][^}]*)?\}")
|
|
21
|
+
_DOLLAR_INPUT_RE = re.compile(r"\$\{inputs\.(\w+)\}")
|
|
22
|
+
|
|
23
|
+
# {env:VAR} — required, kept as-is if missing
|
|
24
|
+
# {env:VAR:default} — fallback to default if env var not set
|
|
25
|
+
_ENV_REF = re.compile(r"\{env:(\w+)(?::([^}]*))?\}")
|
|
26
|
+
|
|
27
|
+
def _resolve_env_refs(value: str) -> str:
|
|
28
|
+
"""Resolve {env:VAR} and {env:VAR:default} placeholders from os.environ."""
|
|
29
|
+
|
|
30
|
+
def _replace(m: re.Match) -> str:
|
|
31
|
+
var = m.group(1)
|
|
32
|
+
default = m.group(2)
|
|
33
|
+
env_val = os.environ.get(var)
|
|
34
|
+
if env_val is not None:
|
|
35
|
+
return env_val
|
|
36
|
+
if default is not None:
|
|
37
|
+
return default
|
|
38
|
+
return m.group(0)
|
|
39
|
+
|
|
40
|
+
return _ENV_REF.sub(_replace, value)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _resolve_input_refs(value: str, inputs: Dict[str, Any]) -> str:
|
|
44
|
+
"""Resolve {input:name} and ${inputs.name} placeholders in a string."""
|
|
45
|
+
|
|
46
|
+
def _replace(m: re.Match) -> str:
|
|
47
|
+
key = m.group(1)
|
|
48
|
+
modifier = m.group(2)
|
|
49
|
+
if key in inputs:
|
|
50
|
+
return str(inputs[key])
|
|
51
|
+
if modifier == "?":
|
|
52
|
+
return ""
|
|
53
|
+
if modifier and modifier[0] in (":", "|"):
|
|
54
|
+
return modifier[1:]
|
|
55
|
+
return m.group(0)
|
|
56
|
+
|
|
57
|
+
result = _INPUT_REF.sub(_replace, value)
|
|
58
|
+
# Also resolve ${inputs.name} syntax
|
|
59
|
+
if "${inputs." in result:
|
|
60
|
+
result = _DOLLAR_INPUT_RE.sub(
|
|
61
|
+
lambda m: str(inputs[m.group(1)]) if m.group(1) in inputs else m.group(0),
|
|
62
|
+
result,
|
|
63
|
+
)
|
|
64
|
+
return result
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _interpolate_parsed(parsed: Dict[str, Any], inputs: Dict[str, Any]) -> None:
|
|
68
|
+
"""Interpolate {input:name} and {env:VAR} refs in body, actions, and content fields."""
|
|
69
|
+
for key in ("body", "content", "raw"):
|
|
70
|
+
if isinstance(parsed.get(key), str):
|
|
71
|
+
parsed[key] = _resolve_env_refs(parsed[key])
|
|
72
|
+
parsed[key] = _resolve_input_refs(parsed[key], inputs)
|
|
73
|
+
|
|
74
|
+
for action in parsed.get("actions", []):
|
|
75
|
+
for k, v in list(action.items()):
|
|
76
|
+
if isinstance(v, str):
|
|
77
|
+
action[k] = _resolve_input_refs(v, inputs)
|
|
78
|
+
for pk, pv in list(action.get("params", {}).items()):
|
|
79
|
+
if isinstance(pv, str):
|
|
80
|
+
action["params"][pk] = _resolve_input_refs(pv, inputs)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def parse_and_validate_directive(
|
|
84
|
+
*,
|
|
85
|
+
file_path: Path,
|
|
86
|
+
item_id: str,
|
|
87
|
+
parameters: Dict[str, Any],
|
|
88
|
+
project_path: Optional[Path] = None,
|
|
89
|
+
) -> Dict[str, Any]:
|
|
90
|
+
"""Parse a directive file, validate inputs, and interpolate placeholders.
|
|
91
|
+
|
|
92
|
+
Returns a dict with ``status`` set to ``"success"`` or ``"error"``.
|
|
93
|
+
|
|
94
|
+
On success the dict also contains:
|
|
95
|
+
parsed – the parsed directive data (with placeholders resolved)
|
|
96
|
+
inputs – the final validated input values (params + defaults)
|
|
97
|
+
declared_inputs – the raw input declarations from the directive
|
|
98
|
+
|
|
99
|
+
On error the dict contains an ``error`` message and, where applicable,
|
|
100
|
+
``item_id`` and ``declared_inputs`` for caller diagnostics.
|
|
101
|
+
"""
|
|
102
|
+
# 1. Integrity check
|
|
103
|
+
try:
|
|
104
|
+
verify_item(file_path, ItemType.DIRECTIVE, project_path=project_path)
|
|
105
|
+
except IntegrityError as exc:
|
|
106
|
+
return {"status": "error", "error": str(exc), "item_id": item_id}
|
|
107
|
+
|
|
108
|
+
# 2. Read and parse
|
|
109
|
+
content = file_path.read_text(encoding="utf-8")
|
|
110
|
+
parsed = ParserRouter().parse("markdown/xml", content)
|
|
111
|
+
|
|
112
|
+
if "error" in parsed:
|
|
113
|
+
return {"status": "error", "error": parsed.get("error"), "item_id": item_id}
|
|
114
|
+
|
|
115
|
+
# 3. Input validation
|
|
116
|
+
inputs = dict(parameters)
|
|
117
|
+
declared_inputs: List[Dict] = parsed.get("inputs", [])
|
|
118
|
+
declared_names = {inp["name"] for inp in declared_inputs}
|
|
119
|
+
|
|
120
|
+
# Reject unknown parameters early so the caller can correct
|
|
121
|
+
unknown = [k for k in parameters if k not in declared_names]
|
|
122
|
+
if unknown and declared_inputs:
|
|
123
|
+
return {
|
|
124
|
+
"status": "error",
|
|
125
|
+
"error": f"Unknown parameters: {', '.join(unknown)}. "
|
|
126
|
+
f"Valid inputs: {', '.join(declared_names)}",
|
|
127
|
+
"item_id": item_id,
|
|
128
|
+
"declared_inputs": declared_inputs,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# Apply defaults
|
|
132
|
+
for inp in declared_inputs:
|
|
133
|
+
if inp["name"] not in inputs and "default" in inp:
|
|
134
|
+
inputs[inp["name"]] = inp["default"]
|
|
135
|
+
|
|
136
|
+
# Check required inputs
|
|
137
|
+
missing = [
|
|
138
|
+
inp["name"]
|
|
139
|
+
for inp in declared_inputs
|
|
140
|
+
if inp.get("required") and inp["name"] not in inputs
|
|
141
|
+
]
|
|
142
|
+
if missing:
|
|
143
|
+
return {
|
|
144
|
+
"status": "error",
|
|
145
|
+
"error": f"Missing required inputs: {', '.join(missing)}",
|
|
146
|
+
"item_id": item_id,
|
|
147
|
+
"declared_inputs": declared_inputs,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# 4. Interpolate placeholders
|
|
151
|
+
_interpolate_parsed(parsed, inputs)
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
"status": "success",
|
|
155
|
+
"parsed": parsed,
|
|
156
|
+
"inputs": inputs,
|
|
157
|
+
"declared_inputs": declared_inputs,
|
|
158
|
+
}
|
rye/executor/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""RYE Executor - Data-driven tool execution with chain resolution.
|
|
2
|
+
|
|
3
|
+
Components:
|
|
4
|
+
- PrimitiveExecutor: Main executor routing tools to Lillux primitives
|
|
5
|
+
- ChainValidator: Validates tool execution chains
|
|
6
|
+
- LockfileResolver: Resolves lockfile paths with 3-tier precedence
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from rye.executor.primitive_executor import PrimitiveExecutor, ExecutionResult
|
|
10
|
+
from rye.executor.chain_validator import ChainValidator, ChainValidationResult
|
|
11
|
+
from rye.executor.lockfile_resolver import LockfileResolver
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"PrimitiveExecutor",
|
|
15
|
+
"ExecutionResult",
|
|
16
|
+
"ChainValidator",
|
|
17
|
+
"ChainValidationResult",
|
|
18
|
+
"LockfileResolver",
|
|
19
|
+
]
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""ChainValidator - Validates tool execution chains before execution.
|
|
2
|
+
|
|
3
|
+
Ensures:
|
|
4
|
+
- Space compatibility (lower precedence cannot depend on higher)
|
|
5
|
+
- I/O compatibility (child outputs match parent inputs)
|
|
6
|
+
- No circular dependencies
|
|
7
|
+
- Version constraints satisfaction
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Any, Dict, List
|
|
13
|
+
|
|
14
|
+
from packaging import version
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class ChainValidationResult:
|
|
21
|
+
"""Result of chain validation."""
|
|
22
|
+
|
|
23
|
+
valid: bool = True
|
|
24
|
+
issues: List[str] = field(default_factory=list)
|
|
25
|
+
warnings: List[str] = field(default_factory=list)
|
|
26
|
+
validated_pairs: int = 0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ChainValidator:
|
|
30
|
+
"""Validates tool execution chains for integrity and compatibility.
|
|
31
|
+
|
|
32
|
+
Validation rules:
|
|
33
|
+
1. Space compatibility: Tools can only depend on equal or higher precedence spaces
|
|
34
|
+
2. I/O compatibility: Child outputs must satisfy parent inputs
|
|
35
|
+
3. Version constraints: Parent's child_constraints must be satisfied
|
|
36
|
+
4. No circular dependencies (handled during chain building)
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
# Space precedence (higher number = higher precedence)
|
|
40
|
+
SPACE_PRECEDENCE = {
|
|
41
|
+
"project": 3,
|
|
42
|
+
"user": 2,
|
|
43
|
+
"system": 1,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
def validate_chain(self, chain: List[Dict[str, Any]]) -> ChainValidationResult:
|
|
47
|
+
"""Validate entire execution chain.
|
|
48
|
+
|
|
49
|
+
Chain order: [tool, runtime, ..., primitive]
|
|
50
|
+
Each pair (chain[i], chain[i+1]) is validated.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
chain: List of chain element dicts with keys:
|
|
54
|
+
- item_id: Tool identifier
|
|
55
|
+
- space: "project", "user", or "system"
|
|
56
|
+
- tool_type: Type of tool
|
|
57
|
+
- executor_id: Delegation target (None for primitives)
|
|
58
|
+
- inputs: Optional list of input types
|
|
59
|
+
- outputs: Optional list of output types
|
|
60
|
+
- version: Optional version string
|
|
61
|
+
- child_constraints: Optional version constraints for dependencies
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
ChainValidationResult with validation details
|
|
65
|
+
"""
|
|
66
|
+
result = ChainValidationResult()
|
|
67
|
+
|
|
68
|
+
if not chain:
|
|
69
|
+
return result
|
|
70
|
+
|
|
71
|
+
if len(chain) == 1:
|
|
72
|
+
# Single element chain (primitive) - no pairs to validate
|
|
73
|
+
return result
|
|
74
|
+
|
|
75
|
+
# Validate each (child, parent) pair
|
|
76
|
+
# In chain order: child → parent (child delegates to parent)
|
|
77
|
+
for i in range(len(chain) - 1):
|
|
78
|
+
child = chain[i]
|
|
79
|
+
parent = chain[i + 1]
|
|
80
|
+
|
|
81
|
+
self._validate_pair(child, parent, result)
|
|
82
|
+
result.validated_pairs += 1
|
|
83
|
+
|
|
84
|
+
# Check for space consistency
|
|
85
|
+
self._validate_space_consistency(chain, result)
|
|
86
|
+
|
|
87
|
+
return result
|
|
88
|
+
|
|
89
|
+
def _validate_pair(
|
|
90
|
+
self,
|
|
91
|
+
child: Dict[str, Any],
|
|
92
|
+
parent: Dict[str, Any],
|
|
93
|
+
result: ChainValidationResult,
|
|
94
|
+
) -> None:
|
|
95
|
+
"""Validate a (child, parent) pair in the chain.
|
|
96
|
+
|
|
97
|
+
Child delegates to parent via executor_id.
|
|
98
|
+
"""
|
|
99
|
+
# 1. Validate space compatibility
|
|
100
|
+
self._validate_space_compatibility(child, parent, result)
|
|
101
|
+
|
|
102
|
+
# 2. Validate I/O compatibility
|
|
103
|
+
self._validate_io_compatibility(child, parent, result)
|
|
104
|
+
|
|
105
|
+
# 3. Validate version constraints
|
|
106
|
+
self._validate_version_constraints(child, parent, result)
|
|
107
|
+
|
|
108
|
+
def _validate_space_compatibility(
|
|
109
|
+
self,
|
|
110
|
+
child: Dict[str, Any],
|
|
111
|
+
parent: Dict[str, Any],
|
|
112
|
+
result: ChainValidationResult,
|
|
113
|
+
) -> None:
|
|
114
|
+
"""Validate that tools from different spaces are compatible.
|
|
115
|
+
|
|
116
|
+
Rule: A tool can depend on tools from equal or higher precedence spaces only.
|
|
117
|
+
|
|
118
|
+
Valid:
|
|
119
|
+
- project → user (project has higher precedence)
|
|
120
|
+
- project → system
|
|
121
|
+
- user → system
|
|
122
|
+
- same space → same space
|
|
123
|
+
|
|
124
|
+
Invalid:
|
|
125
|
+
- user → project (user cannot depend on project-specific tools)
|
|
126
|
+
- system → project/user (system is immutable)
|
|
127
|
+
"""
|
|
128
|
+
child_space = child.get("space", "")
|
|
129
|
+
parent_space = parent.get("space", "")
|
|
130
|
+
|
|
131
|
+
child_precedence = self.SPACE_PRECEDENCE.get(child_space, 0)
|
|
132
|
+
parent_precedence = self.SPACE_PRECEDENCE.get(parent_space, 0)
|
|
133
|
+
|
|
134
|
+
# Lower precedence depending on higher precedence: Invalid
|
|
135
|
+
if child_precedence < parent_precedence:
|
|
136
|
+
result.issues.append(
|
|
137
|
+
f"Tool '{child.get('item_id')}' from {child_space} space cannot "
|
|
138
|
+
f"depend on '{parent.get('item_id')}' from {parent_space} space. "
|
|
139
|
+
f"Lower precedence spaces cannot depend on higher precedence spaces."
|
|
140
|
+
)
|
|
141
|
+
result.valid = False
|
|
142
|
+
|
|
143
|
+
def _validate_io_compatibility(
|
|
144
|
+
self,
|
|
145
|
+
child: Dict[str, Any],
|
|
146
|
+
parent: Dict[str, Any],
|
|
147
|
+
result: ChainValidationResult,
|
|
148
|
+
) -> None:
|
|
149
|
+
"""Validate that child outputs match parent inputs.
|
|
150
|
+
|
|
151
|
+
If both declare I/O types, ensure compatibility.
|
|
152
|
+
Missing declarations are treated as compatible (warnings only).
|
|
153
|
+
"""
|
|
154
|
+
child_outputs = set(child.get("outputs", []))
|
|
155
|
+
parent_inputs = set(parent.get("inputs", []))
|
|
156
|
+
|
|
157
|
+
# Skip if either side doesn't declare types
|
|
158
|
+
if not child_outputs or not parent_inputs:
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
# Check if parent's required inputs are satisfied
|
|
162
|
+
missing = parent_inputs - child_outputs
|
|
163
|
+
|
|
164
|
+
if missing:
|
|
165
|
+
result.issues.append(
|
|
166
|
+
f"I/O mismatch: '{parent.get('item_id')}' requires inputs "
|
|
167
|
+
f"{list(missing)} not provided by '{child.get('item_id')}' "
|
|
168
|
+
f"(outputs: {list(child_outputs)})"
|
|
169
|
+
)
|
|
170
|
+
result.valid = False
|
|
171
|
+
|
|
172
|
+
def _validate_version_constraints(
|
|
173
|
+
self,
|
|
174
|
+
child: Dict[str, Any],
|
|
175
|
+
parent: Dict[str, Any],
|
|
176
|
+
result: ChainValidationResult,
|
|
177
|
+
) -> None:
|
|
178
|
+
"""Validate version constraints between parent and child.
|
|
179
|
+
|
|
180
|
+
Parent can specify child_constraints with min_version/max_version.
|
|
181
|
+
"""
|
|
182
|
+
parent_constraints = parent.get("child_constraints", {})
|
|
183
|
+
child_id = child.get("item_id", "")
|
|
184
|
+
child_version = child.get("version")
|
|
185
|
+
|
|
186
|
+
if not parent_constraints or child_id not in parent_constraints:
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
if not child_version:
|
|
190
|
+
result.warnings.append(
|
|
191
|
+
f"'{child_id}' has no version but '{parent.get('item_id')}' "
|
|
192
|
+
f"specifies version constraints"
|
|
193
|
+
)
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
constraints = parent_constraints[child_id]
|
|
197
|
+
min_version = constraints.get("min_version")
|
|
198
|
+
max_version = constraints.get("max_version")
|
|
199
|
+
|
|
200
|
+
if min_version and not self._version_satisfies(child_version, ">=", min_version):
|
|
201
|
+
result.issues.append(
|
|
202
|
+
f"Version constraint failed: '{child_id}' version {child_version} "
|
|
203
|
+
f"< minimum required {min_version}"
|
|
204
|
+
)
|
|
205
|
+
result.valid = False
|
|
206
|
+
|
|
207
|
+
if max_version and not self._version_satisfies(child_version, "<=", max_version):
|
|
208
|
+
result.issues.append(
|
|
209
|
+
f"Version constraint failed: '{child_id}' version {child_version} "
|
|
210
|
+
f"> maximum allowed {max_version}"
|
|
211
|
+
)
|
|
212
|
+
result.valid = False
|
|
213
|
+
|
|
214
|
+
def _version_satisfies(self, version_str: str, op: str, constraint: str) -> bool:
|
|
215
|
+
"""Check if version satisfies constraint using proper semver.
|
|
216
|
+
|
|
217
|
+
Supports:
|
|
218
|
+
- Standard semver: 1.0.0, 2.1.3
|
|
219
|
+
- Pre-releases: 1.0.0-alpha, 1.0.0-beta.2
|
|
220
|
+
- Build metadata: 1.0.0+build.123
|
|
221
|
+
"""
|
|
222
|
+
try:
|
|
223
|
+
v = version.parse(version_str)
|
|
224
|
+
c = version.parse(constraint)
|
|
225
|
+
|
|
226
|
+
if op == ">=":
|
|
227
|
+
return v >= c
|
|
228
|
+
elif op == "<=":
|
|
229
|
+
return v <= c
|
|
230
|
+
elif op == "==":
|
|
231
|
+
return v == c
|
|
232
|
+
elif op == ">":
|
|
233
|
+
return v > c
|
|
234
|
+
elif op == "<":
|
|
235
|
+
return v < c
|
|
236
|
+
elif op == "!=":
|
|
237
|
+
return v != c
|
|
238
|
+
else:
|
|
239
|
+
logger.warning(f"Unknown version operator: {op}")
|
|
240
|
+
return True
|
|
241
|
+
except version.InvalidVersion:
|
|
242
|
+
logger.warning(f"Invalid version format: {version_str} or {constraint}")
|
|
243
|
+
return True # Invalid versions pass (warning logged)
|
|
244
|
+
|
|
245
|
+
def _validate_space_consistency(
|
|
246
|
+
self,
|
|
247
|
+
chain: List[Dict[str, Any]],
|
|
248
|
+
result: ChainValidationResult,
|
|
249
|
+
) -> None:
|
|
250
|
+
"""Validate overall space consistency in the chain.
|
|
251
|
+
|
|
252
|
+
Additional checks beyond pair validation.
|
|
253
|
+
"""
|
|
254
|
+
# Check if system tools are in the middle of a mutable chain
|
|
255
|
+
spaces = [e.get("space") for e in chain]
|
|
256
|
+
|
|
257
|
+
# Find transitions from system back to mutable
|
|
258
|
+
for i in range(len(spaces) - 1):
|
|
259
|
+
if (spaces[i] or "").startswith("system") and spaces[i + 1] in ("project", "user"):
|
|
260
|
+
result.issues.append(
|
|
261
|
+
f"Invalid chain: system tool '{chain[i].get('item_id')}' "
|
|
262
|
+
f"cannot delegate to mutable {spaces[i + 1]} tool "
|
|
263
|
+
f"'{chain[i + 1].get('item_id')}'"
|
|
264
|
+
)
|
|
265
|
+
result.valid = False
|
|
266
|
+
|
|
267
|
+
def validate_tool(self, tool: Dict[str, Any]) -> ChainValidationResult:
|
|
268
|
+
"""Validate a single tool's metadata.
|
|
269
|
+
|
|
270
|
+
Lightweight validation without chain context.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
tool: Tool metadata dict
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
ChainValidationResult
|
|
277
|
+
"""
|
|
278
|
+
result = ChainValidationResult()
|
|
279
|
+
|
|
280
|
+
# Check required fields
|
|
281
|
+
if not tool.get("item_id"):
|
|
282
|
+
result.issues.append("Missing required field: item_id")
|
|
283
|
+
result.valid = False
|
|
284
|
+
|
|
285
|
+
# Check space is valid
|
|
286
|
+
space = tool.get("space")
|
|
287
|
+
if space and space not in self.SPACE_PRECEDENCE:
|
|
288
|
+
result.issues.append(f"Invalid space: {space}")
|
|
289
|
+
result.valid = False
|
|
290
|
+
|
|
291
|
+
# Check executor_id is valid if present
|
|
292
|
+
executor_id = tool.get("executor_id")
|
|
293
|
+
tool_type = tool.get("tool_type")
|
|
294
|
+
|
|
295
|
+
if tool_type == "primitive" and executor_id is not None:
|
|
296
|
+
result.warnings.append(
|
|
297
|
+
f"Primitive '{tool.get('item_id')}' has executor_id set (should be None)"
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
if tool_type == "runtime" and executor_id is None:
|
|
301
|
+
result.warnings.append(
|
|
302
|
+
f"Runtime '{tool.get('item_id')}' has no executor_id (should delegate)"
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
return result
|