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 +69 -0
- clawzero/adapters/__init__.py +9 -0
- clawzero/adapters/openclaw/__init__.py +182 -0
- clawzero/cli.py +217 -0
- clawzero/contracts.py +63 -0
- clawzero/exceptions.py +34 -0
- clawzero/policies/__init__.py +5 -0
- clawzero/policies/profiles.py +25 -0
- clawzero/protect.py +154 -0
- clawzero/runtime/__init__.py +5 -0
- clawzero/runtime/engine.py +381 -0
- clawzero/witness.py +15 -0
- clawzero/witnesses/__init__.py +15 -0
- clawzero/witnesses/generator.py +160 -0
- clawzero-0.1.0.dist-info/METADATA +192 -0
- clawzero-0.1.0.dist-info/RECORD +20 -0
- clawzero-0.1.0.dist-info/WHEEL +5 -0
- clawzero-0.1.0.dist-info/entry_points.txt +2 -0
- clawzero-0.1.0.dist-info/licenses/LICENSE +17 -0
- clawzero-0.1.0.dist-info/top_level.txt +1 -0
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,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,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
|
+
}
|