clawzero 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.
clawzero/__init__.py ADDED
@@ -0,0 +1,69 @@
1
+ """
2
+ ClawZero - Execution Firewall for AI Agents
3
+
4
+ ClawZero wraps AI agent tools with MVAR runtime governance,
5
+ blocking attacker-influenced executions at critical sinks.
6
+
7
+ Example usage:
8
+ from clawzero import protect
9
+
10
+ def read_file(path: str) -> str:
11
+ with open(path) as f:
12
+ return f.read()
13
+
14
+ safe_read = protect(read_file, sink="filesystem.read", profile="prod_locked")
15
+
16
+ # Blocked: /etc/passwd is in blocklist
17
+ try:
18
+ safe_read("/etc/passwd")
19
+ except ExecutionBlocked as e:
20
+ print(f"Blocked: {e.decision.human_reason}")
21
+
22
+ # Allowed: /workspace is in allowlist
23
+ content = safe_read("/workspace/data.txt")
24
+ """
25
+
26
+ __version__ = "0.1.0"
27
+ __author__ = "MVAR Security"
28
+ __license__ = "Apache-2.0"
29
+
30
+ from clawzero.contracts import ActionDecision, ActionRequest
31
+ from clawzero.adapters import OpenClawAdapter
32
+ from clawzero.exceptions import (
33
+ ClawZeroConfigError,
34
+ ClawZeroError,
35
+ ClawZeroRuntimeError,
36
+ ExecutionBlocked,
37
+ UnsupportedFrameworkError,
38
+ )
39
+ from clawzero.protect import protect
40
+ from clawzero.runtime import MVARRuntime
41
+ from clawzero.witness import (
42
+ WitnessGenerator,
43
+ generate_witness,
44
+ get_witness_generator,
45
+ set_witness_output_dir,
46
+ )
47
+
48
+ __all__ = [
49
+ # Core API
50
+ "protect",
51
+ "MVARRuntime",
52
+ "OpenClawAdapter",
53
+ # Contracts
54
+ "ActionRequest",
55
+ "ActionDecision",
56
+ # Exceptions
57
+ "ExecutionBlocked",
58
+ "ClawZeroError",
59
+ "ClawZeroConfigError",
60
+ "ClawZeroRuntimeError",
61
+ "UnsupportedFrameworkError",
62
+ # Witness generation
63
+ "WitnessGenerator",
64
+ "generate_witness",
65
+ "get_witness_generator",
66
+ "set_witness_output_dir",
67
+ # Adapters (optional import)
68
+ "adapters",
69
+ ]
@@ -0,0 +1,9 @@
1
+ """
2
+ ClawZero Adapters
3
+
4
+ Framework-specific adapters for integrating ClawZero with different AI agent systems.
5
+ """
6
+
7
+ from clawzero.adapters.openclaw import OpenClawAdapter
8
+
9
+ __all__ = ["OpenClawAdapter"]
@@ -0,0 +1,182 @@
1
+ """
2
+ OpenClaw adapter for ClawZero.
3
+
4
+ Integrates OpenClaw tool activity with MVAR enforcement.
5
+ """
6
+
7
+ import uuid
8
+ from typing import Callable, Optional
9
+
10
+ from clawzero.contracts import ActionRequest
11
+ from clawzero.exceptions import ExecutionBlocked
12
+ from clawzero.runtime import MVARRuntime
13
+
14
+
15
+ class OpenClawAdapter:
16
+ """Adapter for integrating ClawZero with OpenClaw runtimes."""
17
+
18
+ ADAPTER_VERSION = "0.1.0"
19
+
20
+ def __init__(
21
+ self,
22
+ profile: str = "dev_balanced",
23
+ agent_id: Optional[str] = None,
24
+ session_id: Optional[str] = None,
25
+ ):
26
+ self.runtime = MVARRuntime(profile=profile)
27
+ self.profile = profile
28
+ self.agent_id = agent_id or "openclaw_agent"
29
+ self.session_id = session_id
30
+
31
+ def wrap_tool(self, tool: Callable, sink_type: Optional[str] = None) -> Callable:
32
+ """Wrap an OpenClaw tool with MVAR enforcement."""
33
+ from functools import wraps
34
+
35
+ if sink_type is None:
36
+ sink_type = self._infer_sink_type(tool)
37
+
38
+ tool_name = getattr(tool, "__name__", str(tool))
39
+
40
+ @wraps(tool)
41
+ def protected_tool(*args, **kwargs):
42
+ target = self._extract_target(tool_name, args, kwargs)
43
+
44
+ request = ActionRequest(
45
+ request_id=str(uuid.uuid4()),
46
+ framework="openclaw",
47
+ agent_id=self.agent_id,
48
+ session_id=self.session_id,
49
+ action_type="tool_call",
50
+ sink_type=sink_type,
51
+ tool_name=tool_name,
52
+ target=target,
53
+ arguments={"args": args, "kwargs": kwargs},
54
+ prompt_provenance=self._build_prompt_provenance(),
55
+ policy_profile=self.profile,
56
+ metadata={
57
+ "adapter": self._build_adapter_metadata(mode="tool_wrap"),
58
+ },
59
+ )
60
+
61
+ decision = self.runtime.evaluate(request)
62
+
63
+ if decision.is_blocked():
64
+ raise ExecutionBlocked(decision)
65
+
66
+ return tool(*args, **kwargs)
67
+
68
+ setattr(protected_tool, "__clawzero_protected__", True)
69
+ setattr(protected_tool, "__clawzero_sink__", sink_type)
70
+
71
+ return protected_tool
72
+
73
+ def intercept_tool_call(self, event: dict) -> None:
74
+ """Intercept and enforce an OpenClaw tool-call event."""
75
+ tool_name = event.get("tool_name", "unknown")
76
+ arguments = event.get("arguments", {})
77
+
78
+ sink_type = self._infer_sink_type_from_name(tool_name)
79
+ target = self._extract_target_from_event(tool_name, arguments)
80
+
81
+ request = ActionRequest(
82
+ request_id=str(uuid.uuid4()),
83
+ framework="openclaw",
84
+ agent_id=self.agent_id,
85
+ session_id=self.session_id,
86
+ action_type="tool_call",
87
+ sink_type=sink_type,
88
+ tool_name=tool_name,
89
+ target=target,
90
+ arguments=arguments,
91
+ prompt_provenance=self._build_prompt_provenance(),
92
+ policy_profile=self.profile,
93
+ metadata={
94
+ "adapter": self._build_adapter_metadata(mode="event_intercept"),
95
+ },
96
+ )
97
+
98
+ decision = self.runtime.evaluate(request)
99
+
100
+ if decision.is_blocked():
101
+ raise ExecutionBlocked(decision)
102
+
103
+ def _build_prompt_provenance(self) -> dict:
104
+ """Return canonical OpenClaw provenance for all adapter request paths."""
105
+ return {
106
+ "source": "openclaw_tool_call",
107
+ "adapter_version": self.ADAPTER_VERSION,
108
+ "framework": "openclaw",
109
+ "taint_level": "untrusted",
110
+ }
111
+
112
+ def _build_adapter_metadata(self, mode: str) -> dict:
113
+ """Return canonical adapter metadata for witness emission."""
114
+ return {
115
+ "name": "openclaw",
116
+ "mode": mode,
117
+ "framework": "openclaw",
118
+ }
119
+
120
+ def _infer_sink_type(self, tool: Callable) -> str:
121
+ tool_name = getattr(tool, "__name__", "").lower()
122
+ return self._infer_sink_type_from_name(tool_name)
123
+
124
+ def _infer_sink_type_from_name(self, tool_name: str) -> str:
125
+ tool_name_lower = tool_name.lower()
126
+
127
+ if any(x in tool_name_lower for x in ["bash", "shell", "exec", "command", "run"]):
128
+ return "shell.exec"
129
+
130
+ if any(x in tool_name_lower for x in ["read", "open", "load", "cat", "view", "show", "get_file"]):
131
+ return "filesystem.read"
132
+
133
+ if any(x in tool_name_lower for x in ["write", "save", "create", "delete", "remove", "mkdir"]):
134
+ return "filesystem.write"
135
+
136
+ if any(x in tool_name_lower for x in ["http", "request", "fetch", "get", "post", "curl", "wget"]):
137
+ return "http.request"
138
+
139
+ if any(x in tool_name_lower for x in ["env", "cred", "credential", "secret", "key", "token", "password"]):
140
+ return "credentials.access"
141
+
142
+ return "tool.custom"
143
+
144
+ def _extract_target(self, tool_name: str, args: tuple, kwargs: dict) -> Optional[str]:
145
+ if "path" in kwargs:
146
+ return str(kwargs["path"])
147
+ if "file" in kwargs:
148
+ return str(kwargs["file"])
149
+ if "filename" in kwargs:
150
+ return str(kwargs["filename"])
151
+ if "command" in kwargs:
152
+ return str(kwargs["command"])
153
+ if "url" in kwargs:
154
+ return str(kwargs["url"])
155
+
156
+ if args:
157
+ return str(args[0])
158
+
159
+ return None
160
+
161
+ def _extract_target_from_event(self, tool_name: str, arguments: dict) -> Optional[str]:
162
+ for key in ["path", "file", "filename", "command", "url", "target"]:
163
+ if key in arguments:
164
+ return str(arguments[key])
165
+
166
+ if arguments:
167
+ return str(next(iter(arguments.values())))
168
+
169
+ return None
170
+
171
+
172
+ def create_openclaw_adapter(
173
+ profile: str = "dev_balanced",
174
+ agent_id: Optional[str] = None,
175
+ session_id: Optional[str] = None,
176
+ ) -> OpenClawAdapter:
177
+ """Convenience constructor for OpenClawAdapter."""
178
+ return OpenClawAdapter(
179
+ profile=profile,
180
+ agent_id=agent_id,
181
+ session_id=session_id,
182
+ )
clawzero/cli.py ADDED
@@ -0,0 +1,217 @@
1
+ """ClawZero command line interface.
2
+
3
+ ClawZero is an in-path enforcement substrate for production agent flows.
4
+ The CLI exposes enforcement-first jobs: demo proof, witness inspection,
5
+ policy audit, and attack replay.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import json
12
+ import subprocess
13
+ import sys
14
+ import uuid
15
+ from pathlib import Path
16
+
17
+ from clawzero.contracts import ActionRequest
18
+ from clawzero.runtime import MVARRuntime
19
+
20
+
21
+ def _repo_root() -> Path:
22
+ return Path(__file__).resolve().parents[2]
23
+
24
+
25
+ def _run_openclaw_demo(mode: str, scenario: str) -> int:
26
+ demo_script = _repo_root() / "demo" / "openclaw_attack_demo.py"
27
+ if not demo_script.exists():
28
+ print(f"Demo script not found: {demo_script}", file=sys.stderr)
29
+ return 2
30
+
31
+ cmd = [sys.executable, str(demo_script), "--mode", mode, "--scenario", scenario]
32
+ proc = subprocess.run(cmd, check=False)
33
+ return proc.returncode
34
+
35
+
36
+ def _cmd_demo_openclaw(args: argparse.Namespace) -> int:
37
+ return _run_openclaw_demo(mode=args.mode, scenario=args.scenario)
38
+
39
+
40
+ def _cmd_attack_replay(args: argparse.Namespace) -> int:
41
+ # Attack replay is intentionally routed through the same enforcement demo.
42
+ return _run_openclaw_demo(mode="compare", scenario=args.scenario)
43
+
44
+
45
+ def _cmd_audit_decision(args: argparse.Namespace) -> int:
46
+ runtime = MVARRuntime(profile=args.profile)
47
+
48
+ taint_markers = [m.strip() for m in args.taint_markers.split(",") if m.strip()]
49
+ request = ActionRequest(
50
+ request_id=str(uuid.uuid4()),
51
+ framework="openclaw",
52
+ action_type="tool_call",
53
+ sink_type=args.sink_type,
54
+ tool_name=args.tool_name,
55
+ target=args.target,
56
+ arguments={"command": args.command},
57
+ prompt_provenance={
58
+ "source": args.source,
59
+ "taint_level": args.taint_level,
60
+ "source_chain": [args.source, "openclaw_tool_call"],
61
+ "taint_markers": taint_markers,
62
+ },
63
+ policy_profile=args.profile,
64
+ metadata={
65
+ "adapter": {
66
+ "name": "openclaw",
67
+ "mode": "tool_wrap",
68
+ "framework": "openclaw",
69
+ }
70
+ },
71
+ )
72
+
73
+ decision = runtime.evaluate(request)
74
+
75
+ print("ClawZero Enforcement Audit")
76
+ print("-" * 32)
77
+ print(f"decision : {decision.decision}")
78
+ print(f"reason : {decision.reason_code}")
79
+ print(f"human : {decision.human_reason}")
80
+ print(f"sink : {decision.sink_type}")
81
+ print(f"target : {decision.target}")
82
+ print(f"policy_id : {decision.policy_id}")
83
+ print(f"engine : {decision.engine}")
84
+ if runtime.last_witness:
85
+ print(f"witness_id : {runtime.last_witness.get('witness_id')}")
86
+ return 0
87
+
88
+
89
+ def _cmd_witness_show(args: argparse.Namespace) -> int:
90
+ path = Path(args.file)
91
+ if not path.exists():
92
+ print(f"Witness file not found: {path}", file=sys.stderr)
93
+ return 2
94
+
95
+ witness = json.loads(path.read_text(encoding="utf-8"))
96
+ print(json.dumps(witness, indent=2))
97
+ return 0
98
+
99
+
100
+ def _cmd_witness_verify(args: argparse.Namespace) -> int:
101
+ path = Path(args.file)
102
+ if not path.exists():
103
+ print(f"Witness file not found: {path}", file=sys.stderr)
104
+ return 2
105
+
106
+ witness = json.loads(path.read_text(encoding="utf-8"))
107
+ required = {
108
+ "timestamp",
109
+ "agent_runtime",
110
+ "sink_type",
111
+ "target",
112
+ "decision",
113
+ "reason_code",
114
+ "policy_id",
115
+ "engine",
116
+ "provenance",
117
+ "adapter",
118
+ "witness_signature",
119
+ }
120
+ missing = sorted(required.difference(witness.keys()))
121
+ if missing:
122
+ print("invalid witness")
123
+ print(f"missing keys: {', '.join(missing)}")
124
+ return 1
125
+
126
+ print("witness valid")
127
+ print(f"decision: {witness.get('decision')}")
128
+ print(f"policy : {witness.get('policy_id')}")
129
+ return 0
130
+
131
+
132
+ def build_parser() -> argparse.ArgumentParser:
133
+ parser = argparse.ArgumentParser(
134
+ prog="clawzero",
135
+ description=(
136
+ "ClawZero: deterministic in-path execution boundary for OpenClaw agent flows."
137
+ ),
138
+ )
139
+ subparsers = parser.add_subparsers(dest="command", required=True)
140
+
141
+ demo = subparsers.add_parser(
142
+ "demo",
143
+ help="Run enforcement proof demos (same input, different boundary).",
144
+ )
145
+ demo_sub = demo.add_subparsers(dest="demo_command", required=True)
146
+ demo_openclaw = demo_sub.add_parser(
147
+ "openclaw",
148
+ help="Run OpenClaw demo through standard vs MVAR-protected paths.",
149
+ )
150
+ demo_openclaw.add_argument("--mode", choices=["standard", "mvar", "compare"], default="compare")
151
+ demo_openclaw.add_argument(
152
+ "--scenario", choices=["shell", "credentials", "benign"], default="shell"
153
+ )
154
+ demo_openclaw.set_defaults(func=_cmd_demo_openclaw)
155
+
156
+ witness = subparsers.add_parser(
157
+ "witness",
158
+ help="Inspect and validate signed witness artifacts from enforcement decisions.",
159
+ )
160
+ witness_sub = witness.add_subparsers(dest="witness_command", required=True)
161
+ witness_show = witness_sub.add_parser("show", help="Print a witness JSON artifact.")
162
+ witness_show.add_argument("--file", required=True, help="Path to witness JSON file.")
163
+ witness_show.set_defaults(func=_cmd_witness_show)
164
+
165
+ witness_verify = witness_sub.add_parser(
166
+ "verify", help="Verify required canonical fields in a witness artifact."
167
+ )
168
+ witness_verify.add_argument("--file", required=True, help="Path to witness JSON file.")
169
+ witness_verify.set_defaults(func=_cmd_witness_verify)
170
+
171
+ audit = subparsers.add_parser(
172
+ "audit",
173
+ help="Audit deterministic policy enforcement for a specific sink request.",
174
+ )
175
+ audit_sub = audit.add_subparsers(dest="audit_command", required=True)
176
+ audit_decision = audit_sub.add_parser(
177
+ "decision", help="Evaluate a single request through the active MVAR runtime."
178
+ )
179
+ audit_decision.add_argument("--profile", default="prod_locked")
180
+ audit_decision.add_argument("--sink-type", required=True)
181
+ audit_decision.add_argument("--target", required=True)
182
+ audit_decision.add_argument("--tool-name", default="tool_call")
183
+ audit_decision.add_argument("--command", default="")
184
+ audit_decision.add_argument("--source", default="external_document")
185
+ audit_decision.add_argument("--taint-level", default="untrusted")
186
+ audit_decision.add_argument("--taint-markers", default="prompt_injection,external_content")
187
+ audit_decision.set_defaults(func=_cmd_audit_decision)
188
+
189
+ attack = subparsers.add_parser(
190
+ "attack",
191
+ help="Replay known attack scenarios to prove sink-boundary enforcement.",
192
+ )
193
+ attack_sub = attack.add_subparsers(dest="attack_command", required=True)
194
+ attack_replay = attack_sub.add_parser(
195
+ "replay",
196
+ help="Run compare-mode attack replay (standard compromised vs MVAR blocked).",
197
+ )
198
+ attack_replay.add_argument(
199
+ "--scenario", choices=["shell", "credentials", "benign"], default="shell"
200
+ )
201
+ attack_replay.set_defaults(func=_cmd_attack_replay)
202
+
203
+ return parser
204
+
205
+
206
+ def main(argv: list[str] | None = None) -> int:
207
+ parser = build_parser()
208
+ args = parser.parse_args(argv)
209
+ func = getattr(args, "func", None)
210
+ if func is None:
211
+ parser.print_help()
212
+ return 2
213
+ return int(func(args))
214
+
215
+
216
+ if __name__ == "__main__":
217
+ raise SystemExit(main())
clawzero/contracts.py ADDED
@@ -0,0 +1,63 @@
1
+ """
2
+ ClawZero contracts.
3
+
4
+ Data contracts for execution-boundary requests and decisions.
5
+ """
6
+
7
+ from dataclasses import dataclass, field
8
+ from typing import Any, Optional
9
+
10
+
11
+ @dataclass
12
+ class ActionRequest:
13
+ """A request entering the execution boundary."""
14
+
15
+ request_id: str
16
+ framework: str
17
+
18
+ agent_id: Optional[str] = None
19
+ session_id: Optional[str] = None
20
+
21
+ action_type: str = "tool_call"
22
+ sink_type: str = "tool.custom"
23
+
24
+ tool_name: Optional[str] = None
25
+ target: Optional[str] = None
26
+
27
+ arguments: dict[str, Any] = field(default_factory=dict)
28
+ prompt_provenance: dict[str, Any] = field(default_factory=dict)
29
+ conversation_context: dict[str, Any] = field(default_factory=dict)
30
+
31
+ policy_profile: str = "dev_balanced"
32
+ metadata: dict[str, Any] = field(default_factory=dict)
33
+
34
+
35
+ @dataclass
36
+ class ActionDecision:
37
+ """Deterministic policy decision emitted by the runtime."""
38
+
39
+ request_id: str
40
+ decision: str
41
+ reason_code: str
42
+ human_reason: str
43
+
44
+ sink_type: str
45
+ target: Optional[str]
46
+ policy_profile: str
47
+
48
+ engine: str = "embedded-policy-v0.1"
49
+ policy_id: str = "mvar-embedded.v0.1"
50
+
51
+ trust_level: Optional[str] = None
52
+ witness_id: Optional[str] = None
53
+
54
+ annotations: dict[str, Any] = field(default_factory=dict)
55
+
56
+ def is_blocked(self) -> bool:
57
+ return self.decision == "block"
58
+
59
+ def is_allowed(self) -> bool:
60
+ return self.decision == "allow"
61
+
62
+ def is_annotated(self) -> bool:
63
+ return self.decision == "annotate"
clawzero/exceptions.py ADDED
@@ -0,0 +1,34 @@
1
+ """ClawZero exceptions."""
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from clawzero.contracts import ActionDecision
7
+
8
+
9
+ class ClawZeroError(Exception):
10
+ """Base exception for all ClawZero errors."""
11
+
12
+
13
+ class ExecutionBlocked(ClawZeroError):
14
+ """Raised when MVAR blocks an action from reaching a protected sink."""
15
+
16
+ def __init__(self, decision: "ActionDecision"):
17
+ self.decision = decision
18
+ message = f"MVAR blocked: {decision.reason_code} — {decision.human_reason}"
19
+ super().__init__(message)
20
+
21
+ def __str__(self) -> str:
22
+ return f"MVAR blocked: {self.decision.reason_code} — {self.decision.human_reason}"
23
+
24
+
25
+ class ClawZeroConfigError(ClawZeroError):
26
+ """Raised when ClawZero configuration is invalid or missing."""
27
+
28
+
29
+ class ClawZeroRuntimeError(ClawZeroError):
30
+ """Raised when ClawZero encounters an unexpected runtime error."""
31
+
32
+
33
+ class UnsupportedFrameworkError(ClawZeroError):
34
+ """Raised when attempting to protect a tool from an unsupported framework."""
@@ -0,0 +1,5 @@
1
+ """Policy definitions for deterministic sink enforcement."""
2
+
3
+ from clawzero.policies.profiles import PROFILES
4
+
5
+ __all__ = ["PROFILES"]
@@ -0,0 +1,25 @@
1
+ """Embedded policy profile metadata for documentation and tooling."""
2
+
3
+ PROFILES = {
4
+ "dev_balanced": {
5
+ "shell.exec": "block",
6
+ "filesystem.read": "profile_sensitive",
7
+ "http.request": "allow",
8
+ "credentials.access": "block",
9
+ "tool.custom": "allow",
10
+ },
11
+ "dev_strict": {
12
+ "shell.exec": "block",
13
+ "filesystem.read": "allow /workspace only",
14
+ "http.request": "block",
15
+ "credentials.access": "block",
16
+ "tool.custom": "annotate",
17
+ },
18
+ "prod_locked": {
19
+ "shell.exec": "block",
20
+ "filesystem.read": "allow /workspace/project only",
21
+ "http.request": "allow localhost only",
22
+ "credentials.access": "block",
23
+ "tool.custom": "allow",
24
+ },
25
+ }