Skip to content

Commit 59ce80d

Browse files
committed
openclaw adapter
1 parent fe3e7ea commit 59ce80d

8 files changed

Lines changed: 1062 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ secure_agent.run()
6464
| LangChain | Supported |
6565
| Playwright | Supported |
6666
| PydanticAI | Supported |
67-
| OpenClaw | Planned |
67+
| OpenClaw | Supported |
6868

6969
## Architecture
7070

docs/user-manual.md

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,133 @@ secure_agent = SecureAgent(
482482

483483
---
484484

485+
### OpenClaw
486+
487+
[OpenClaw](https://github.com/openclaw/openclaw) is a local-first AI agent framework that connects to messaging platforms. SecureAgent integrates with OpenClaw CLI via HTTP proxy interception.
488+
489+
#### Architecture
490+
491+
The OpenClaw adapter works by:
492+
1. Starting an HTTP proxy server that intercepts OpenClaw skill calls
493+
2. Enforcing authorization policies before forwarding to actual skills
494+
3. Managing the OpenClaw CLI subprocess lifecycle
495+
496+
```
497+
Python SecureAgent
498+
499+
├─── HTTP Proxy (localhost:8788) ───┐
500+
│ ▼
501+
└─── spawns ──────► OpenClaw CLI ──► predicate-snapshot skill
502+
503+
└─► Browser actions (authorized)
504+
```
505+
506+
#### Basic Usage
507+
508+
```python
509+
from predicate_secure import SecureAgent
510+
from predicate_secure.openclaw_adapter import OpenClawConfig
511+
512+
# Create OpenClaw configuration
513+
openclaw_config = OpenClawConfig(
514+
cli_path="/usr/local/bin/openclaw", # Or None to use PATH
515+
skill_proxy_port=8788,
516+
skill_name="predicate-snapshot",
517+
)
518+
519+
# Or use a dict:
520+
# openclaw_config = {
521+
# "openclaw_cli_path": "/usr/local/bin/openclaw",
522+
# "skill_proxy_port": 8788,
523+
# }
524+
525+
# Wrap with SecureAgent
526+
secure_agent = SecureAgent(
527+
agent=openclaw_config,
528+
policy="policies/openclaw.yaml",
529+
mode="strict",
530+
)
531+
532+
# Run a task
533+
result = secure_agent.run(task="Navigate to example.com and take a snapshot")
534+
```
535+
536+
#### Policy Example for OpenClaw
537+
538+
```yaml
539+
# policies/openclaw.yaml
540+
rules:
541+
# Allow snapshot skill
542+
- action: "openclaw.skill.predicate-snapshot"
543+
resource: "*"
544+
effect: allow
545+
546+
# Allow clicking elements
547+
- action: "openclaw.skill.predicate-act.click"
548+
resource: "element:*"
549+
effect: allow
550+
551+
# Allow typing (but not in password fields)
552+
- action: "openclaw.skill.predicate-act.type"
553+
resource: "element:*"
554+
effect: allow
555+
conditions:
556+
- not_contains: ["password", "ssn"]
557+
558+
# Block scroll actions
559+
- action: "openclaw.skill.predicate-act.scroll"
560+
resource: "*"
561+
effect: deny
562+
563+
# Default deny
564+
- action: "*"
565+
resource: "*"
566+
effect: deny
567+
```
568+
569+
#### Proxy Configuration
570+
571+
The HTTP proxy intercepts requests to OpenClaw skills:
572+
573+
```python
574+
from predicate_secure.openclaw_adapter import create_openclaw_adapter, OpenClawConfig
575+
576+
config = OpenClawConfig(skill_proxy_port=8788)
577+
578+
# Custom authorizer function
579+
def my_authorizer(action: str, context: dict) -> bool:
580+
# Custom authorization logic
581+
if "snapshot" in action:
582+
return True
583+
print(f"Blocked: {action}")
584+
return False
585+
586+
adapter = create_openclaw_adapter(config, authorizer=my_authorizer)
587+
588+
# Start proxy
589+
adapter.start_proxy()
590+
print("Proxy running on http://localhost:8788")
591+
592+
# ... run OpenClaw tasks ...
593+
594+
# Cleanup
595+
adapter.cleanup()
596+
```
597+
598+
#### Environment Variables
599+
600+
Configure OpenClaw skill to use the proxy:
601+
602+
```bash
603+
export PREDICATE_PROXY_URL="http://localhost:8788"
604+
```
605+
606+
The OpenClaw adapter automatically sets this when starting the CLI subprocess.
607+
608+
**Full example:** [examples/openclaw_browser_automation.py](../examples/openclaw_browser_automation.py)
609+
610+
---
611+
485612
## Modes
486613

487614
SecureAgent supports four execution modes:
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
"""
2+
Example: Secure OpenClaw agent with authorization and verification.
3+
4+
This example demonstrates how to wrap an OpenClaw CLI agent with predicate-secure
5+
to add pre-action authorization and audit logging for browser automation tasks.
6+
7+
Prerequisites:
8+
1. OpenClaw CLI installed (npm install -g openclaw)
9+
2. predicate-snapshot skill installed in OpenClaw
10+
3. Policy file (policies/openclaw_browser.yaml)
11+
"""
12+
13+
from pathlib import Path
14+
15+
from predicate_secure import SecureAgent
16+
from predicate_secure.openclaw_adapter import OpenClawConfig
17+
18+
# Example policy file content (create this as policies/openclaw_browser.yaml):
19+
EXAMPLE_POLICY = """
20+
rules:
21+
# Allow OpenClaw snapshot skill
22+
- action: "openclaw.skill.predicate-snapshot"
23+
resource: "*"
24+
effect: allow
25+
26+
# Allow clicking on known safe domains
27+
- action: "openclaw.skill.predicate-act.click"
28+
resource: "element:*"
29+
effect: allow
30+
conditions:
31+
# Only allow on safe domains (you'd check current URL in real policy)
32+
- domain_matches: ["*.example.com", "*.trusted-site.com"]
33+
34+
# Allow typing in form fields (with restrictions)
35+
- action: "openclaw.skill.predicate-act.type"
36+
resource: "element:*"
37+
effect: allow
38+
conditions:
39+
# Prevent entering sensitive data
40+
- not_contains: ["password", "ssn", "credit"]
41+
42+
# Block scrolling to prevent UI confusion
43+
- action: "openclaw.skill.predicate-act.scroll"
44+
resource: "*"
45+
effect: deny
46+
47+
# Default deny for safety
48+
- action: "*"
49+
resource: "*"
50+
effect: deny
51+
"""
52+
53+
54+
def main():
55+
"""Run OpenClaw agent with secure authorization."""
56+
# Create OpenClaw configuration
57+
openclaw_config = OpenClawConfig(
58+
cli_path="/usr/local/bin/openclaw", # Or None to use PATH
59+
skill_proxy_port=8788, # Port for HTTP proxy
60+
skill_name="predicate-snapshot",
61+
working_dir=str(Path.home() / ".openclaw"),
62+
)
63+
64+
# You could also use a dict instead of OpenClawConfig:
65+
# openclaw_config = {
66+
# "openclaw_cli_path": "/usr/local/bin/openclaw",
67+
# "skill_proxy_port": 8788,
68+
# "skill_name": "predicate-snapshot",
69+
# }
70+
71+
# Create policy file
72+
policy_dir = Path("policies")
73+
policy_dir.mkdir(exist_ok=True)
74+
policy_file = policy_dir / "openclaw_browser.yaml"
75+
76+
if not policy_file.exists():
77+
policy_file.write_text(EXAMPLE_POLICY)
78+
print(f"Created example policy at {policy_file}")
79+
80+
# Wrap OpenClaw with SecureAgent
81+
secure_agent = SecureAgent(
82+
agent=openclaw_config,
83+
policy=str(policy_file),
84+
mode="strict", # Fail-closed mode
85+
principal_id="openclaw-agent-01",
86+
trace_format="console",
87+
)
88+
89+
print(f"[predicate-secure] Detected framework: {secure_agent.framework.value}")
90+
print(f"[predicate-secure] Mode: {secure_agent.config.mode}")
91+
print(f"[predicate-secure] Policy: {secure_agent.config.effective_policy_path}")
92+
93+
# Example task
94+
task = "Navigate to example.com and take a snapshot"
95+
96+
print(f"\n[OpenClaw] Running task: {task}")
97+
print("[predicate-secure] Starting HTTP proxy for skill interception...")
98+
99+
try:
100+
# Run the OpenClaw task with authorization
101+
result = secure_agent.run(task=task)
102+
print(f"\n[OpenClaw] Task completed successfully")
103+
print(f"Return code: {result.get('returncode', 'N/A')}")
104+
print(f"Output: {result.get('stdout', '')[:200]}...")
105+
except Exception as e:
106+
print(f"\n[predicate-secure] Task failed: {e}")
107+
108+
109+
def example_with_debug_mode():
110+
"""Run OpenClaw agent in debug mode for troubleshooting."""
111+
openclaw_config = OpenClawConfig(skill_proxy_port=8789)
112+
113+
secure_agent = SecureAgent(
114+
agent=openclaw_config,
115+
mode="debug", # Human-readable trace output
116+
trace_format="console",
117+
trace_colors=True,
118+
)
119+
120+
print("\n[Debug Mode] Running OpenClaw agent with full tracing...")
121+
122+
task = "Check if example.com loads correctly"
123+
124+
try:
125+
result = secure_agent.run(task=task)
126+
print("\n[Debug] Task trace complete")
127+
except Exception as e:
128+
print(f"\n[Debug] Error occurred: {e}")
129+
130+
131+
def example_with_manual_proxy():
132+
"""
133+
Example showing how to manually control the proxy lifecycle.
134+
135+
Useful when you want to keep the proxy running across multiple tasks.
136+
"""
137+
from predicate_secure.openclaw_adapter import create_openclaw_adapter
138+
139+
openclaw_config = OpenClawConfig(skill_proxy_port=8790)
140+
141+
# Create adapter manually
142+
def authorizer(action: str, context: dict) -> bool:
143+
"""Simple authorizer that allows snapshot but blocks act."""
144+
if "snapshot" in action:
145+
return True
146+
print(f"[Authorizer] Blocked action: {action}")
147+
return False
148+
149+
adapter = create_openclaw_adapter(openclaw_config, authorizer=authorizer)
150+
151+
try:
152+
# Start proxy (stays running)
153+
adapter.start_proxy()
154+
print("[Proxy] Started on http://localhost:8790")
155+
156+
# Run multiple tasks with same proxy
157+
tasks = [
158+
"Take snapshot of example.com",
159+
"Take snapshot of httpbin.org",
160+
]
161+
162+
for task in tasks:
163+
print(f"\n[Task] {task}")
164+
adapter.start_cli(task)
165+
# Process would run in background
166+
# In real usage, you'd wait for completion
167+
168+
finally:
169+
# Clean up
170+
adapter.cleanup()
171+
print("\n[Proxy] Stopped")
172+
173+
174+
if __name__ == "__main__":
175+
print("=" * 60)
176+
print("predicate-secure: OpenClaw Agent Example")
177+
print("=" * 60)
178+
179+
# Uncomment the example you want to run:
180+
181+
# Example 1: Basic usage with policy file
182+
# main()
183+
184+
# Example 2: Debug mode with full tracing
185+
# example_with_debug_mode()
186+
187+
# Example 3: Manual proxy control
188+
# example_with_manual_proxy()
189+
190+
print("\nNote: Uncomment one of the example functions in __main__ to run")
191+
print("Make sure OpenClaw CLI is installed and in your PATH")

src/predicate_secure/__init__.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,8 @@ def run(self, task: str | None = None) -> Any:
410410
result = self._run_langchain(task)
411411
elif self._wrapped.framework == Framework.PYDANTIC_AI.value:
412412
result = self._run_pydantic_ai(task)
413+
elif self._wrapped.framework == Framework.OPENCLAW.value:
414+
result = self._run_openclaw(task)
413415
else:
414416
raise NotImplementedError(
415417
f"run() not implemented for framework: {self._wrapped.framework}"
@@ -481,6 +483,52 @@ def _run_pydantic_ai(self, task: str | None) -> Any:
481483
"""Run PydanticAI agent with authorization."""
482484
raise NotImplementedError("PydanticAI integration not yet implemented.")
483485

486+
def _run_openclaw(self, task: str | None) -> Any:
487+
"""Run OpenClaw CLI agent with authorization."""
488+
try:
489+
from .openclaw_adapter import OpenClawAdapter, create_openclaw_adapter
490+
except ImportError:
491+
raise NotImplementedError(
492+
"OpenClaw integration requires openclaw_adapter module. "
493+
"Ensure all dependencies are installed."
494+
)
495+
496+
# Get or create adapter
497+
if not hasattr(self._wrapped, "openclaw_adapter"):
498+
# Create adapter from original agent config
499+
authorizer = self._create_pre_action_authorizer()
500+
adapter = create_openclaw_adapter(self._wrapped.original, authorizer)
501+
self._wrapped.metadata["openclaw_adapter"] = adapter
502+
else:
503+
adapter = self._wrapped.metadata.get("openclaw_adapter")
504+
505+
if not isinstance(adapter, OpenClawAdapter):
506+
raise ValueError("Invalid OpenClaw adapter")
507+
508+
# Start proxy server
509+
adapter.start_proxy()
510+
511+
try:
512+
# Start CLI with task
513+
if task is None:
514+
raise ValueError("Task is required for OpenClaw agents")
515+
516+
process = adapter.start_cli(task)
517+
518+
# Wait for completion
519+
stdout, stderr = process.communicate()
520+
521+
# Check for errors
522+
if process.returncode != 0:
523+
raise RuntimeError(f"OpenClaw CLI failed: {stderr}")
524+
525+
return {"stdout": stdout, "stderr": stderr, "returncode": process.returncode}
526+
527+
finally:
528+
# Cleanup
529+
adapter.stop_cli()
530+
adapter.stop_proxy()
531+
484532
def trace_step(
485533
self,
486534
action: str,

0 commit comments

Comments
 (0)