loom-agent 0.0.1__py3-none-any.whl → 0.0.2__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.
Potentially problematic release.
This version of loom-agent might be problematic. Click here for more details.
- loom/builtin/tools/calculator.py +4 -0
- loom/builtin/tools/document_search.py +5 -0
- loom/builtin/tools/glob.py +4 -0
- loom/builtin/tools/grep.py +4 -0
- loom/builtin/tools/http_request.py +5 -0
- loom/builtin/tools/python_repl.py +5 -0
- loom/builtin/tools/read_file.py +4 -0
- loom/builtin/tools/task.py +5 -0
- loom/builtin/tools/web_search.py +4 -0
- loom/builtin/tools/write_file.py +4 -0
- loom/components/agent.py +121 -5
- loom/core/agent_executor.py +505 -320
- loom/core/compression_manager.py +17 -10
- loom/core/context_assembly.py +329 -0
- loom/core/events.py +414 -0
- loom/core/execution_context.py +119 -0
- loom/core/tool_orchestrator.py +383 -0
- loom/core/turn_state.py +188 -0
- loom/core/types.py +15 -4
- loom/interfaces/event_producer.py +172 -0
- loom/interfaces/tool.py +22 -1
- loom/security/__init__.py +13 -0
- loom/security/models.py +85 -0
- loom/security/path_validator.py +128 -0
- loom/security/validator.py +346 -0
- loom/tasks/PHASE_1_FOUNDATION/task_1.1_agent_events.md +121 -0
- loom/tasks/PHASE_1_FOUNDATION/task_1.2_streaming_api.md +521 -0
- loom/tasks/PHASE_1_FOUNDATION/task_1.3_context_assembler.md +606 -0
- loom/tasks/PHASE_2_CORE_FEATURES/task_2.1_tool_orchestrator.md +743 -0
- loom/tasks/PHASE_2_CORE_FEATURES/task_2.2_security_validator.md +676 -0
- loom/tasks/README.md +109 -0
- loom/tasks/__init__.py +11 -0
- loom/tasks/sql_placeholder.py +100 -0
- loom_agent-0.0.2.dist-info/METADATA +295 -0
- {loom_agent-0.0.1.dist-info → loom_agent-0.0.2.dist-info}/RECORD +37 -19
- loom_agent-0.0.1.dist-info/METADATA +0 -457
- {loom_agent-0.0.1.dist-info → loom_agent-0.0.2.dist-info}/WHEEL +0 -0
- {loom_agent-0.0.1.dist-info → loom_agent-0.0.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Event Producer Protocol for Loom 2.0
|
|
3
|
+
|
|
4
|
+
Defines the interface that all event-producing components must implement.
|
|
5
|
+
This enables type-safe composition of streaming agents.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Protocol, AsyncGenerator, runtime_checkable
|
|
9
|
+
from loom.core.events import AgentEvent
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@runtime_checkable
|
|
13
|
+
class EventProducer(Protocol):
|
|
14
|
+
"""
|
|
15
|
+
Protocol for components that produce AgentEvent streams.
|
|
16
|
+
|
|
17
|
+
Any component that participates in the agent execution pipeline
|
|
18
|
+
and produces events should implement this protocol.
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
```python
|
|
22
|
+
class MyCustomExecutor(EventProducer):
|
|
23
|
+
async def produce_events(self) -> AsyncGenerator[AgentEvent, None]:
|
|
24
|
+
yield AgentEvent.phase_start("custom_phase")
|
|
25
|
+
# ... do work
|
|
26
|
+
yield AgentEvent.phase_end("custom_phase")
|
|
27
|
+
```
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
async def produce_events(self) -> AsyncGenerator[AgentEvent, None]:
|
|
31
|
+
"""
|
|
32
|
+
Produce a stream of agent events.
|
|
33
|
+
|
|
34
|
+
Yields:
|
|
35
|
+
AgentEvent: Events representing execution progress
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
```python
|
|
39
|
+
async for event in producer.produce_events():
|
|
40
|
+
if event.type == AgentEventType.LLM_DELTA:
|
|
41
|
+
print(event.content, end="")
|
|
42
|
+
```
|
|
43
|
+
"""
|
|
44
|
+
...
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@runtime_checkable
|
|
48
|
+
class ToolExecutor(Protocol):
|
|
49
|
+
"""
|
|
50
|
+
Protocol for tool execution components that produce events.
|
|
51
|
+
|
|
52
|
+
This is a specialized EventProducer for tool execution.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
async def execute_tool(
|
|
56
|
+
self,
|
|
57
|
+
tool_name: str,
|
|
58
|
+
arguments: dict
|
|
59
|
+
) -> AsyncGenerator[AgentEvent, None]:
|
|
60
|
+
"""
|
|
61
|
+
Execute a tool and yield progress events.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
tool_name: Name of the tool to execute
|
|
65
|
+
arguments: Tool arguments
|
|
66
|
+
|
|
67
|
+
Yields:
|
|
68
|
+
AgentEvent: Tool execution events (TOOL_EXECUTION_START,
|
|
69
|
+
TOOL_PROGRESS, TOOL_RESULT, or TOOL_ERROR)
|
|
70
|
+
"""
|
|
71
|
+
...
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@runtime_checkable
|
|
75
|
+
class LLMEventProducer(Protocol):
|
|
76
|
+
"""
|
|
77
|
+
Protocol for LLM components that produce streaming events.
|
|
78
|
+
|
|
79
|
+
This enables streaming LLM calls with real-time token generation.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
async def stream_with_events(
|
|
83
|
+
self,
|
|
84
|
+
messages: list,
|
|
85
|
+
tools: list = None
|
|
86
|
+
) -> AsyncGenerator[AgentEvent, None]:
|
|
87
|
+
"""
|
|
88
|
+
Stream LLM generation as AgentEvents.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
messages: Conversation messages
|
|
92
|
+
tools: Optional tool definitions
|
|
93
|
+
|
|
94
|
+
Yields:
|
|
95
|
+
AgentEvent: LLM events (LLM_START, LLM_DELTA, LLM_COMPLETE,
|
|
96
|
+
LLM_TOOL_CALLS)
|
|
97
|
+
"""
|
|
98
|
+
...
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ===== Helper Functions =====
|
|
102
|
+
|
|
103
|
+
async def merge_event_streams(
|
|
104
|
+
*producers: EventProducer
|
|
105
|
+
) -> AsyncGenerator[AgentEvent, None]:
|
|
106
|
+
"""
|
|
107
|
+
Merge multiple event streams into a single stream.
|
|
108
|
+
|
|
109
|
+
This is useful for parallel execution where multiple components
|
|
110
|
+
produce events concurrently.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
*producers: EventProducer instances to merge
|
|
114
|
+
|
|
115
|
+
Yields:
|
|
116
|
+
AgentEvent: Events from all producers in arrival order
|
|
117
|
+
|
|
118
|
+
Example:
|
|
119
|
+
```python
|
|
120
|
+
async for event in merge_event_streams(executor1, executor2):
|
|
121
|
+
print(event)
|
|
122
|
+
```
|
|
123
|
+
"""
|
|
124
|
+
import asyncio
|
|
125
|
+
|
|
126
|
+
# Create tasks for all producers
|
|
127
|
+
tasks = [
|
|
128
|
+
asyncio.create_task(_consume_producer(producer))
|
|
129
|
+
for producer in producers
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
# Yield events as they arrive
|
|
133
|
+
pending = set(tasks)
|
|
134
|
+
while pending:
|
|
135
|
+
done, pending = await asyncio.wait(
|
|
136
|
+
pending,
|
|
137
|
+
return_when=asyncio.FIRST_COMPLETED
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
for task in done:
|
|
141
|
+
events = task.result()
|
|
142
|
+
for event in events:
|
|
143
|
+
yield event
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
async def _consume_producer(producer: EventProducer) -> list:
|
|
147
|
+
"""Helper to consume a producer into a list"""
|
|
148
|
+
events = []
|
|
149
|
+
async for event in producer.produce_events():
|
|
150
|
+
events.append(event)
|
|
151
|
+
return events
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
async def collect_events(
|
|
155
|
+
producer: EventProducer
|
|
156
|
+
) -> list:
|
|
157
|
+
"""
|
|
158
|
+
Collect all events from a producer into a list.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
producer: EventProducer to consume
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
List of all events produced
|
|
165
|
+
|
|
166
|
+
Example:
|
|
167
|
+
```python
|
|
168
|
+
events = await collect_events(my_executor)
|
|
169
|
+
print(f"Generated {len(events)} events")
|
|
170
|
+
```
|
|
171
|
+
"""
|
|
172
|
+
return await _consume_producer(producer)
|
loom/interfaces/tool.py
CHANGED
|
@@ -7,12 +7,32 @@ from pydantic import BaseModel
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class BaseTool(ABC):
|
|
10
|
-
"""
|
|
10
|
+
"""
|
|
11
|
+
工具基础接口。
|
|
12
|
+
|
|
13
|
+
Attributes:
|
|
14
|
+
name: Tool name (unique identifier)
|
|
15
|
+
description: Tool description for LLM
|
|
16
|
+
args_schema: Pydantic model for argument validation
|
|
17
|
+
is_read_only: Whether tool only reads data (safe to parallelize) 🆕
|
|
18
|
+
category: Tool category (general/destructive/network) 🆕
|
|
19
|
+
requires_confirmation: Whether tool requires user confirmation 🆕
|
|
20
|
+
"""
|
|
11
21
|
|
|
12
22
|
name: str
|
|
13
23
|
description: str
|
|
14
24
|
args_schema: type[BaseModel]
|
|
15
25
|
|
|
26
|
+
# 🆕 Loom 2.0 - Orchestration attributes
|
|
27
|
+
is_read_only: bool = False
|
|
28
|
+
"""Whether this tool only reads data (safe to parallelize with other read-only tools)."""
|
|
29
|
+
|
|
30
|
+
category: str = "general"
|
|
31
|
+
"""Tool category: 'general', 'destructive', 'network'."""
|
|
32
|
+
|
|
33
|
+
requires_confirmation: bool = False
|
|
34
|
+
"""Whether this tool requires explicit user confirmation before execution."""
|
|
35
|
+
|
|
16
36
|
@abstractmethod
|
|
17
37
|
async def run(self, **kwargs) -> Any:
|
|
18
38
|
raise NotImplementedError
|
|
@@ -23,5 +43,6 @@ class BaseTool(ABC):
|
|
|
23
43
|
|
|
24
44
|
@property
|
|
25
45
|
def is_concurrency_safe(self) -> bool:
|
|
46
|
+
"""Legacy attribute for backward compatibility."""
|
|
26
47
|
return True
|
|
27
48
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Security module for Loom 2.0"""
|
|
2
|
+
|
|
3
|
+
from loom.security.models import RiskLevel, SecurityDecision, PathSecurityResult
|
|
4
|
+
from loom.security.path_validator import PathSecurityValidator
|
|
5
|
+
from loom.security.validator import SecurityValidator
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"RiskLevel",
|
|
9
|
+
"SecurityDecision",
|
|
10
|
+
"PathSecurityResult",
|
|
11
|
+
"PathSecurityValidator",
|
|
12
|
+
"SecurityValidator",
|
|
13
|
+
]
|
loom/security/models.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Security Models
|
|
3
|
+
|
|
4
|
+
Data models for security validation results.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import List, Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RiskLevel(str, Enum):
|
|
13
|
+
"""Security risk levels for tool execution."""
|
|
14
|
+
LOW = "low"
|
|
15
|
+
MEDIUM = "medium"
|
|
16
|
+
HIGH = "high"
|
|
17
|
+
CRITICAL = "critical"
|
|
18
|
+
|
|
19
|
+
def __lt__(self, other):
|
|
20
|
+
"""Allow comparison of risk levels."""
|
|
21
|
+
order = {
|
|
22
|
+
RiskLevel.LOW: 0,
|
|
23
|
+
RiskLevel.MEDIUM: 1,
|
|
24
|
+
RiskLevel.HIGH: 2,
|
|
25
|
+
RiskLevel.CRITICAL: 3
|
|
26
|
+
}
|
|
27
|
+
return order[self] < order[other]
|
|
28
|
+
|
|
29
|
+
def __gt__(self, other):
|
|
30
|
+
order = {
|
|
31
|
+
RiskLevel.LOW: 0,
|
|
32
|
+
RiskLevel.MEDIUM: 1,
|
|
33
|
+
RiskLevel.HIGH: 2,
|
|
34
|
+
RiskLevel.CRITICAL: 3
|
|
35
|
+
}
|
|
36
|
+
return order[self] > order[other]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class SecurityDecision:
|
|
41
|
+
"""
|
|
42
|
+
Result of security validation.
|
|
43
|
+
|
|
44
|
+
Attributes:
|
|
45
|
+
allow: Whether the operation is allowed
|
|
46
|
+
risk_level: Assessed risk level
|
|
47
|
+
reason: Human-readable reason for decision
|
|
48
|
+
failed_layers: List of security layers that failed
|
|
49
|
+
warnings: Non-blocking warnings
|
|
50
|
+
"""
|
|
51
|
+
allow: bool
|
|
52
|
+
risk_level: RiskLevel
|
|
53
|
+
reason: str
|
|
54
|
+
failed_layers: List[str] = field(default_factory=list)
|
|
55
|
+
warnings: List[str] = field(default_factory=list)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def is_safe(self) -> bool:
|
|
59
|
+
"""Check if decision is safe to execute."""
|
|
60
|
+
return self.allow and self.risk_level in [RiskLevel.LOW, RiskLevel.MEDIUM]
|
|
61
|
+
|
|
62
|
+
def __repr__(self) -> str:
|
|
63
|
+
status = "ALLOWED" if self.allow else "BLOCKED"
|
|
64
|
+
return f"SecurityDecision({status}, risk={self.risk_level.value}, reason='{self.reason}')"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class PathSecurityResult:
|
|
69
|
+
"""
|
|
70
|
+
Result of path security validation.
|
|
71
|
+
|
|
72
|
+
Attributes:
|
|
73
|
+
is_safe: Whether the path is safe to access
|
|
74
|
+
normalized_path: Resolved absolute path
|
|
75
|
+
warnings: Non-critical warnings
|
|
76
|
+
violations: Security violations found
|
|
77
|
+
"""
|
|
78
|
+
is_safe: bool
|
|
79
|
+
normalized_path: str
|
|
80
|
+
warnings: List[str] = field(default_factory=list)
|
|
81
|
+
violations: List[str] = field(default_factory=list)
|
|
82
|
+
|
|
83
|
+
def __repr__(self) -> str:
|
|
84
|
+
status = "SAFE" if self.is_safe else "UNSAFE"
|
|
85
|
+
return f"PathSecurityResult({status}, violations={len(self.violations)})"
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Path Security Validator
|
|
3
|
+
|
|
4
|
+
Validates file paths for security issues like path traversal and system path access.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
from loom.security.models import PathSecurityResult
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# System paths that should never be accessed
|
|
13
|
+
SYSTEM_PATHS = [
|
|
14
|
+
"/etc",
|
|
15
|
+
"/sys",
|
|
16
|
+
"/proc",
|
|
17
|
+
"/dev",
|
|
18
|
+
"/boot",
|
|
19
|
+
"/root",
|
|
20
|
+
"/var/log",
|
|
21
|
+
"/bin",
|
|
22
|
+
"/sbin",
|
|
23
|
+
"/usr/bin",
|
|
24
|
+
"/usr/sbin",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class PathSecurityValidator:
|
|
29
|
+
"""
|
|
30
|
+
Validate file paths for security issues.
|
|
31
|
+
|
|
32
|
+
Checks for:
|
|
33
|
+
- Path traversal attacks (../)
|
|
34
|
+
- Absolute paths outside working directory
|
|
35
|
+
- System path access
|
|
36
|
+
- Invalid path constructions
|
|
37
|
+
|
|
38
|
+
Example:
|
|
39
|
+
```python
|
|
40
|
+
validator = PathSecurityValidator(working_dir=Path("/Users/project"))
|
|
41
|
+
|
|
42
|
+
# Safe path
|
|
43
|
+
result = validator.validate_path("src/main.py")
|
|
44
|
+
assert result.is_safe
|
|
45
|
+
|
|
46
|
+
# Path traversal attempt
|
|
47
|
+
result = validator.validate_path("../../etc/passwd")
|
|
48
|
+
assert not result.is_safe
|
|
49
|
+
```
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, working_dir: Optional[Path] = None):
|
|
53
|
+
"""
|
|
54
|
+
Initialize path validator.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
working_dir: Working directory to enforce boundaries (defaults to cwd)
|
|
58
|
+
"""
|
|
59
|
+
self.working_dir = (working_dir or Path.cwd()).resolve()
|
|
60
|
+
|
|
61
|
+
def validate_path(self, path: str) -> PathSecurityResult:
|
|
62
|
+
"""
|
|
63
|
+
Validate a file path for security issues.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
path: File path to validate
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
PathSecurityResult with validation outcome
|
|
70
|
+
"""
|
|
71
|
+
violations: List[str] = []
|
|
72
|
+
warnings: List[str] = []
|
|
73
|
+
normalized_path = path
|
|
74
|
+
|
|
75
|
+
# Check 1: Detect explicit path traversal
|
|
76
|
+
if ".." in path:
|
|
77
|
+
violations.append("Path traversal detected (..)")
|
|
78
|
+
|
|
79
|
+
# Check 2: Resolve and validate boundaries
|
|
80
|
+
try:
|
|
81
|
+
# Handle both relative and absolute paths
|
|
82
|
+
if Path(path).is_absolute():
|
|
83
|
+
resolved = Path(path).resolve()
|
|
84
|
+
else:
|
|
85
|
+
resolved = (self.working_dir / path).resolve()
|
|
86
|
+
|
|
87
|
+
normalized_path = str(resolved)
|
|
88
|
+
|
|
89
|
+
# Check if within working directory
|
|
90
|
+
try:
|
|
91
|
+
resolved.relative_to(self.working_dir)
|
|
92
|
+
except ValueError:
|
|
93
|
+
violations.append(
|
|
94
|
+
f"Path outside working directory: {resolved} "
|
|
95
|
+
f"(working dir: {self.working_dir})"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Check 3: System path protection
|
|
99
|
+
for sys_path in SYSTEM_PATHS:
|
|
100
|
+
if str(resolved).startswith(sys_path):
|
|
101
|
+
violations.append(
|
|
102
|
+
f"System path access denied: {sys_path}"
|
|
103
|
+
)
|
|
104
|
+
break
|
|
105
|
+
|
|
106
|
+
except Exception as e:
|
|
107
|
+
violations.append(f"Path resolution failed: {e}")
|
|
108
|
+
|
|
109
|
+
is_safe = len(violations) == 0
|
|
110
|
+
|
|
111
|
+
return PathSecurityResult(
|
|
112
|
+
is_safe=is_safe,
|
|
113
|
+
normalized_path=normalized_path,
|
|
114
|
+
warnings=warnings,
|
|
115
|
+
violations=violations
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def is_safe_path(self, path: str) -> bool:
|
|
119
|
+
"""
|
|
120
|
+
Quick check if path is safe.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
path: Path to check
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
True if path is safe
|
|
127
|
+
"""
|
|
128
|
+
return self.validate_path(path).is_safe
|