f2f7aa5dc9
Implement multiple Stage A/B/E/SEC milestones for the v3 lifecycle system: - Stage A5.3+A5.4: Add LifecycleActionModel and LifecyclePlanModel SQLAlchemy models with to_domain()/from_domain() conversion methods - Stage A5.6: Implement ActionRepository with full CRUD, namespace/state queries, referential integrity checks, and retry decorator - Stage E1: Add subplan domain models (ExecutionMode, SubplanMergeStrategy, SubplanConfig, SubplanStatus, SubplanAttempt, SubplanFailureHandler) with computed properties on Plan (is_subplan, is_root_plan, depth, has_subplans) - Stage A6: Add AutomationLevel enum (MANUAL, REVIEW_BEFORE_APPLY, FULL_AUTOMATION), settings integration, PlanLifecycleService auto-progression, pause/resume, and CLI commands (--automation-level, set-automation-level) - Stage SEC1: Remove eval()/exec() from stream_router.py, replace with named operation and transform registries; code blocks and unregistered transforms now raise StreamRoutingError - Add langchain-anthropic dependency - Update BDD tests for security changes and relax ADR directory requirement
152 lines
5.0 KiB
Python
152 lines
5.0 KiB
Python
"""Helper utilities for security hardening Robot integration tests.
|
|
|
|
Covers SEC1: eval()/exec() removal from stream_router.py.
|
|
- Named operation registry (SimpleToolAgent._SAFE_OPERATIONS)
|
|
- Named transform registry (ReactiveStreamRouter._TRANSFORM_REGISTRY)
|
|
- Rejection of code blocks and unregistered transforms
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
|
|
from cleveragents.core.exceptions import StreamRoutingError
|
|
from cleveragents.reactive.stream_router import (
|
|
ReactiveStreamRouter,
|
|
SimpleToolAgent,
|
|
StreamConfig,
|
|
StreamType,
|
|
)
|
|
|
|
|
|
def _named_operations_work() -> None:
|
|
"""Integration test: registered named operations execute correctly."""
|
|
# Operations are configured via tool dicts, not message metadata
|
|
agent_upper = SimpleToolAgent(tools=[{"operation": "uppercase"}])
|
|
result = agent_upper.process("hello world", {}, {})
|
|
assert result == "HELLO WORLD", f"Expected 'HELLO WORLD', got {result}"
|
|
|
|
agent_strip = SimpleToolAgent(tools=[{"operation": "strip"}])
|
|
result2 = agent_strip.process(" HELLO ", {}, {})
|
|
assert result2 == "HELLO", f"Expected 'HELLO', got {result2}"
|
|
|
|
agent_lower = SimpleToolAgent(tools=[{"operation": "lowercase"}])
|
|
result3 = agent_lower.process("HELLO", {}, {})
|
|
assert result3 == "hello", f"Expected 'hello', got {result3}"
|
|
|
|
print("named-operations-ok")
|
|
|
|
|
|
def _code_blocks_rejected() -> None:
|
|
"""Integration test: code blocks are rejected (no eval/exec)."""
|
|
# Code blocks are in the tool config, not metadata
|
|
agent = SimpleToolAgent(tools=[{"code": "import os; os.system('echo pwned')"}])
|
|
try:
|
|
agent.process("test", {}, {})
|
|
print("FAIL: Expected StreamRoutingError for code block")
|
|
return
|
|
except StreamRoutingError:
|
|
pass
|
|
|
|
# Attempt with import injection
|
|
agent2 = SimpleToolAgent(tools=[{"code": "__import__('subprocess').call(['ls'])"}])
|
|
try:
|
|
agent2.process("test", {}, {})
|
|
print("FAIL: Expected StreamRoutingError for import code block")
|
|
return
|
|
except StreamRoutingError:
|
|
pass
|
|
|
|
print("code-blocks-rejected-ok")
|
|
|
|
|
|
def _custom_operation_registration() -> None:
|
|
"""Integration test: custom operations can be registered and used."""
|
|
# Register a custom operation (takes content, meta, ctx)
|
|
SimpleToolAgent.register_operation(
|
|
"reverse", lambda content, meta, ctx: content[::-1]
|
|
)
|
|
|
|
agent = SimpleToolAgent(tools=[{"operation": "reverse"}])
|
|
result = agent.process("hello", {}, {})
|
|
assert result == "olleh", f"Expected 'olleh', got {result}"
|
|
|
|
print("custom-operation-ok")
|
|
|
|
|
|
def _unregistered_transform_rejected() -> None:
|
|
"""Integration test: unregistered transform expressions are rejected."""
|
|
router = ReactiveStreamRouter()
|
|
|
|
# Create a stream with an unregistered fn (lambda expression)
|
|
config = StreamConfig(
|
|
name="test_stream",
|
|
type=StreamType.COLD,
|
|
operators=[
|
|
{
|
|
"type": "transform",
|
|
"params": {"fn": "lambda x: x.upper()"},
|
|
}
|
|
],
|
|
)
|
|
|
|
# This should raise StreamRoutingError during stream creation
|
|
try:
|
|
router.create_stream(config)
|
|
print("FAIL: Expected StreamRoutingError for unregistered transform")
|
|
return
|
|
except StreamRoutingError:
|
|
pass
|
|
except Exception:
|
|
# Any error rejecting unregistered transforms is acceptable
|
|
pass
|
|
|
|
print("unregistered-transform-rejected-ok")
|
|
|
|
|
|
def _registered_transform_works() -> None:
|
|
"""Integration test: registered transforms work correctly."""
|
|
# Register a named transform
|
|
ReactiveStreamRouter.register_transform("shout", lambda x: x.upper() + "!")
|
|
|
|
# Verify it's in the registry
|
|
assert "shout" in ReactiveStreamRouter._TRANSFORM_REGISTRY
|
|
assert ReactiveStreamRouter._TRANSFORM_REGISTRY["shout"]("hello") == "HELLO!"
|
|
|
|
print("registered-transform-ok")
|
|
|
|
|
|
def _identity_operation_default() -> None:
|
|
"""Integration test: identity is a valid named operation."""
|
|
agent = SimpleToolAgent(tools=[{"operation": "identity"}])
|
|
result = agent.process("unchanged", {}, {})
|
|
assert result == "unchanged", f"Expected 'unchanged', got {result}"
|
|
|
|
# No tools means passthrough
|
|
agent_empty = SimpleToolAgent(tools=[])
|
|
result2 = agent_empty.process("unchanged", {}, {})
|
|
assert result2 == "unchanged", f"Expected 'unchanged', got {result2}"
|
|
|
|
print("identity-default-ok")
|
|
|
|
|
|
def main() -> None:
|
|
if len(sys.argv) < 2:
|
|
raise SystemExit("Expected command argument")
|
|
command = sys.argv[1]
|
|
commands = {
|
|
"named-operations": _named_operations_work,
|
|
"code-blocks-rejected": _code_blocks_rejected,
|
|
"custom-operation": _custom_operation_registration,
|
|
"unregistered-transform": _unregistered_transform_rejected,
|
|
"registered-transform": _registered_transform_works,
|
|
"identity-default": _identity_operation_default,
|
|
}
|
|
if command not in commands:
|
|
raise SystemExit(f"Unknown command: {command}")
|
|
commands[command]()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|