handcuff 0.2.3__tar.gz → 0.3.0__tar.gz
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.
- {handcuff-0.2.3 → handcuff-0.3.0}/.gitignore +2 -1
- {handcuff-0.2.3 → handcuff-0.3.0}/PKG-INFO +3 -1
- handcuff-0.3.0/handcuff/integrations/langchain_sandbox.py +36 -0
- handcuff-0.3.0/handcuff/sandbox/__init__.py +4 -0
- handcuff-0.3.0/handcuff/sandbox/manager.py +184 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/pyproject.toml +4 -1
- handcuff-0.2.3/01KX3HRHQKZHGVE0TWJV1SH6MX.json +0 -118
- handcuff-0.2.3/Dummy Agentic Project/README.md +0 -36
- handcuff-0.2.3/Dummy Agentic Project/agent_output.txt +0 -1
- handcuff-0.2.3/Dummy Agentic Project/main.py +0 -71
- handcuff-0.2.3/Dummy Agentic Project/requirements.txt +0 -5
- {handcuff-0.2.3 → handcuff-0.3.0}/.github/workflows/publish.yml +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/00_README_START_HERE.md +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/README.md +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/__init__.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/alerts/__init__.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/alerts/webhooks.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/banner.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/capture/__init__.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/capture/base.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/capture/file_watch.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/capture/net_watch.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/capture/psutil_backend.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/capture/redact.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/capture/tree.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/cli.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/config.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/core/__init__.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/core/bus.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/core/events.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/core/hashing.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/core/session.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/core/signing.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/doctor.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/enforce/__init__.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/enforce/file_acl.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/enforce/firewall.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/enforce/scan.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/export/__init__.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/export/html_export.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/export/json_export.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/export/md_export.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/gateway/__init__.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/gateway/gateway.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/gateway/log.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/gateway/policy.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/integrations/__init__.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/integrations/langchain.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/replay.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/rules/__init__.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/rules/defaults.yaml +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/rules/dsl.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/rules/engine.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/rules/injection.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/rules/trust.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/runner.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/storage/__init__.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/storage/db.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/storage/writer.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/tui/__init__.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/tui/app.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/tui/risk.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/tui/styles.tcss +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/tui/theme.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/tui/widgets/__init__.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/tui/widgets/banner_header.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/tui/widgets/flag_cards.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/tui/widgets/footer.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/tui/widgets/hero.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/tui/widgets/logs.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/tui/widgets/monitoring.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/tui/widgets/recommendation.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/tui/widgets/risk_overview.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/tui/widgets/trust.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/handcuff/verify.py +0 -0
- {handcuff-0.2.3 → handcuff-0.3.0}/scripts/build_windows.ps1 +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: handcuff
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A dashcam for your AI agents: local, tamper-evident recorder for process/file/network activity, with a LangChain/LangGraph drop-in prompt-injection circuit breaker.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Mister2005/Handcuff
|
|
6
6
|
Project-URL: Repository, https://github.com/Mister2005/Handcuff
|
|
@@ -38,6 +38,8 @@ Requires-Dist: pytest>=8.2; extra == 'dev'
|
|
|
38
38
|
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
39
39
|
Provides-Extra: langchain
|
|
40
40
|
Requires-Dist: langchain-core>=0.2; extra == 'langchain'
|
|
41
|
+
Provides-Extra: sandbox
|
|
42
|
+
Requires-Dist: docker>=7.0.0; extra == 'sandbox'
|
|
41
43
|
Description-Content-Type: text/markdown
|
|
42
44
|
|
|
43
45
|
# Handcuff — Documentation Index (Start Here)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""LangChain tools that run inside a Docker Sandbox"""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from langchain_core.tools import tool
|
|
5
|
+
|
|
6
|
+
from handcuff.sandbox.manager import DockerSandbox, SandboxError, QuarantineViolation
|
|
7
|
+
|
|
8
|
+
@tool
|
|
9
|
+
def sandboxed_download(url: str, dest_filename: str) -> str:
|
|
10
|
+
"""Download a file from a URL securely into an isolated Sandbox.
|
|
11
|
+
The file is scanned for prompt injection and malicious signatures
|
|
12
|
+
before being copied to the local workspace.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
url: The URL to download.
|
|
16
|
+
dest_filename: The local filename to save the safe file to.
|
|
17
|
+
"""
|
|
18
|
+
try:
|
|
19
|
+
with DockerSandbox() as sandbox:
|
|
20
|
+
container_path = "/tmp/downloaded_file"
|
|
21
|
+
# Execute python inside the container to download, safely passing url via env var
|
|
22
|
+
cmd = f"""python -c "import os, urllib.request; urllib.request.urlretrieve(os.environ['DOWNLOAD_URL'], '{container_path}')" """
|
|
23
|
+
sandbox.execute(cmd, environment={"DOWNLOAD_URL": url})
|
|
24
|
+
|
|
25
|
+
# Copy to host through the quarantine scanner
|
|
26
|
+
host_path = Path.cwd() / dest_filename
|
|
27
|
+
sandbox.copy_quarantined(container_path, host_path)
|
|
28
|
+
|
|
29
|
+
return f"Successfully downloaded and verified safe. Saved to {dest_filename}"
|
|
30
|
+
|
|
31
|
+
except SandboxError as e:
|
|
32
|
+
return f"Sandbox execution failed: {e}"
|
|
33
|
+
except QuarantineViolation as e:
|
|
34
|
+
return f"QUARANTINE BLOCKED: File was malicious and destroyed in the sandbox. {e}"
|
|
35
|
+
except Exception as e:
|
|
36
|
+
return f"Error: {e}"
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Docker Sandbox Manager for Handcuff Quarantine.
|
|
2
|
+
|
|
3
|
+
Provides an ephemeral Docker container for executing risky agent
|
|
4
|
+
tools (e.g. downloading datasets, cloning repos) without risking
|
|
5
|
+
the host machine.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import io
|
|
10
|
+
import os
|
|
11
|
+
import tarfile
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
import docker
|
|
16
|
+
_HAS_DOCKER = True
|
|
17
|
+
except ImportError:
|
|
18
|
+
_HAS_DOCKER = False
|
|
19
|
+
|
|
20
|
+
from handcuff.rules.injection import scan_for_injection
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SandboxError(Exception):
|
|
24
|
+
"""Raised for general sandbox execution failures."""
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class QuarantineViolation(PermissionError):
|
|
29
|
+
"""Raised when a file fails heuristic quarantine checks and is blocked."""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DockerSandbox:
|
|
34
|
+
"""Ephemeral Docker container manager for secure execution and quarantine."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, image: str = "python:3.11-slim"):
|
|
37
|
+
if not _HAS_DOCKER:
|
|
38
|
+
raise ImportError("The 'docker' package is required. Run: pip install handcuff[sandbox]")
|
|
39
|
+
|
|
40
|
+
self.image = image
|
|
41
|
+
try:
|
|
42
|
+
self.client = docker.from_env()
|
|
43
|
+
# Test connection
|
|
44
|
+
self.client.ping()
|
|
45
|
+
except docker.errors.DockerException as e:
|
|
46
|
+
raise SandboxError(f"Could not connect to Docker Daemon. Is Docker running? Details: {e}")
|
|
47
|
+
|
|
48
|
+
self.container = None
|
|
49
|
+
|
|
50
|
+
def __enter__(self) -> DockerSandbox:
|
|
51
|
+
self.start()
|
|
52
|
+
return self
|
|
53
|
+
|
|
54
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
55
|
+
self.stop()
|
|
56
|
+
|
|
57
|
+
def start(self) -> None:
|
|
58
|
+
"""Starts the ephemeral container."""
|
|
59
|
+
try:
|
|
60
|
+
# Ensure image exists locally, pull if not
|
|
61
|
+
try:
|
|
62
|
+
self.client.images.get(self.image)
|
|
63
|
+
except docker.errors.ImageNotFound:
|
|
64
|
+
self.client.images.pull(self.image)
|
|
65
|
+
|
|
66
|
+
self.container = self.client.containers.run(
|
|
67
|
+
self.image,
|
|
68
|
+
command="tail -f /dev/null", # Keep alive
|
|
69
|
+
detach=True,
|
|
70
|
+
network_mode="bridge",
|
|
71
|
+
remove=True, # Auto-remove when stopped
|
|
72
|
+
cap_drop=["ALL"], # Drop all privileges for security
|
|
73
|
+
mem_limit="512m"
|
|
74
|
+
)
|
|
75
|
+
except Exception as e:
|
|
76
|
+
raise SandboxError(f"Failed to start Docker sandbox: {e}")
|
|
77
|
+
|
|
78
|
+
def stop(self) -> None:
|
|
79
|
+
"""Stops and destroys the container."""
|
|
80
|
+
if self.container:
|
|
81
|
+
try:
|
|
82
|
+
self.container.remove(force=True)
|
|
83
|
+
except Exception:
|
|
84
|
+
pass
|
|
85
|
+
self.container = None
|
|
86
|
+
|
|
87
|
+
def execute(self, cmd: str, timeout: int = 30, environment: dict | None = None) -> str:
|
|
88
|
+
"""Execute a command inside the container and return stdout.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
cmd: Command to execute
|
|
92
|
+
timeout: Maximum execution time in seconds.
|
|
93
|
+
environment: Dictionary of environment variables to pass to the command.
|
|
94
|
+
"""
|
|
95
|
+
if not self.container:
|
|
96
|
+
raise SandboxError("Sandbox is not running.")
|
|
97
|
+
|
|
98
|
+
import threading
|
|
99
|
+
result = {"exit_code": -1, "output": b""}
|
|
100
|
+
exc = []
|
|
101
|
+
|
|
102
|
+
def run_cmd():
|
|
103
|
+
try:
|
|
104
|
+
# exec_run returns (exit_code, output)
|
|
105
|
+
exit_code, output = self.container.exec_run(cmd, environment=environment)
|
|
106
|
+
result["exit_code"] = exit_code
|
|
107
|
+
result["output"] = output
|
|
108
|
+
except Exception as e:
|
|
109
|
+
exc.append(e)
|
|
110
|
+
|
|
111
|
+
t = threading.Thread(target=run_cmd)
|
|
112
|
+
t.start()
|
|
113
|
+
t.join(timeout=timeout)
|
|
114
|
+
|
|
115
|
+
if t.is_alive():
|
|
116
|
+
# If still alive, it timed out. Force kill the container.
|
|
117
|
+
self.stop()
|
|
118
|
+
raise SandboxError(f"Command execution timed out after {timeout} seconds. Container was destroyed.")
|
|
119
|
+
|
|
120
|
+
if exc:
|
|
121
|
+
raise SandboxError(f"Failed to execute command: {exc[0]}")
|
|
122
|
+
|
|
123
|
+
if result["exit_code"] != 0:
|
|
124
|
+
raise SandboxError(f"Command failed (exit {result['exit_code']}): {result['output'].decode('utf-8', errors='replace')}")
|
|
125
|
+
|
|
126
|
+
return result["output"].decode('utf-8', errors='replace')
|
|
127
|
+
|
|
128
|
+
def copy_quarantined(self, container_path: str, host_path: str | Path) -> None:
|
|
129
|
+
"""
|
|
130
|
+
Copy a file from the container to the host, running heuristic scans
|
|
131
|
+
before persisting it to the host filesystem.
|
|
132
|
+
"""
|
|
133
|
+
if not self.container:
|
|
134
|
+
raise SandboxError("Sandbox is not running.")
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
bits, stat = self.container.get_archive(container_path)
|
|
138
|
+
except docker.errors.NotFound:
|
|
139
|
+
raise FileNotFoundError(f"File {container_path} not found in sandbox.")
|
|
140
|
+
|
|
141
|
+
# get_archive returns a tar stream. We extract it in memory.
|
|
142
|
+
tar_stream = io.BytesIO()
|
|
143
|
+
max_size = 100 * 1024 * 1024 # 100 MB limit
|
|
144
|
+
current_size = 0
|
|
145
|
+
|
|
146
|
+
for chunk in bits:
|
|
147
|
+
current_size += len(chunk)
|
|
148
|
+
if current_size > max_size:
|
|
149
|
+
raise SandboxError("File exceeds the 100MB quarantine extraction limit.")
|
|
150
|
+
tar_stream.write(chunk)
|
|
151
|
+
|
|
152
|
+
tar_stream.seek(0)
|
|
153
|
+
with tarfile.open(fileobj=tar_stream, mode='r') as tar:
|
|
154
|
+
members = tar.getmembers()
|
|
155
|
+
if not members:
|
|
156
|
+
raise SandboxError("Empty archive returned from container.")
|
|
157
|
+
|
|
158
|
+
# We only support single file extraction for now
|
|
159
|
+
file_member = members[0]
|
|
160
|
+
if not file_member.isfile():
|
|
161
|
+
raise SandboxError(f"{container_path} is not a file.")
|
|
162
|
+
|
|
163
|
+
extracted = tar.extractfile(file_member)
|
|
164
|
+
if not extracted:
|
|
165
|
+
raise SandboxError(f"Could not read {file_member.name} from archive.")
|
|
166
|
+
|
|
167
|
+
content = extracted.read()
|
|
168
|
+
|
|
169
|
+
# --- QUARANTINE SCAN ---
|
|
170
|
+
# Decode as string to run heuristic prompt injection scan
|
|
171
|
+
try:
|
|
172
|
+
text_content = content.decode('utf-8')
|
|
173
|
+
findings = scan_for_injection(text_content)
|
|
174
|
+
if findings:
|
|
175
|
+
reasons = ", ".join(f.reason for f in findings)
|
|
176
|
+
raise QuarantineViolation(f"Quarantine block! Malicious patterns detected: {reasons}")
|
|
177
|
+
except UnicodeDecodeError:
|
|
178
|
+
# Binary file, skip text injection scanning
|
|
179
|
+
# We could add yara rules or malware scanning here in the future
|
|
180
|
+
pass
|
|
181
|
+
|
|
182
|
+
# --- PERSIST TO HOST ---
|
|
183
|
+
with open(host_path, 'wb') as f:
|
|
184
|
+
f.write(content)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "handcuff"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "A dashcam for your AI agents: local, tamper-evident recorder for process/file/network activity, with a LangChain/LangGraph drop-in prompt-injection circuit breaker."
|
|
5
5
|
readme = "00_README_START_HERE.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -59,6 +59,9 @@ dev = [
|
|
|
59
59
|
langchain = [
|
|
60
60
|
"langchain-core>=0.2",
|
|
61
61
|
]
|
|
62
|
+
sandbox = [
|
|
63
|
+
"docker>=7.0.0",
|
|
64
|
+
]
|
|
62
65
|
|
|
63
66
|
[tool.ruff]
|
|
64
67
|
line-length = 100
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"session": {
|
|
3
|
-
"id": "01KX3HRHQKZHGVE0TWJV1SH6MX",
|
|
4
|
-
"label": null,
|
|
5
|
-
"target_cmd": "python main.py",
|
|
6
|
-
"root_pid": 40008,
|
|
7
|
-
"started_at": 1783604463347,
|
|
8
|
-
"ended_at": 1783604464847,
|
|
9
|
-
"status": "CLOSED"
|
|
10
|
-
},
|
|
11
|
-
"events": [
|
|
12
|
-
{
|
|
13
|
-
"seq": 0,
|
|
14
|
-
"ts": 1783604463387,
|
|
15
|
-
"kind": "PROCESS_SPAWN",
|
|
16
|
-
"pid": 40008,
|
|
17
|
-
"ppid": 40008,
|
|
18
|
-
"payload": {
|
|
19
|
-
"argv": [
|
|
20
|
-
"python",
|
|
21
|
-
"main.py"
|
|
22
|
-
],
|
|
23
|
-
"cwd": "C:\\Users\\Varun\\Desktop\\Main\\Personal Learning\\Handcuff\\Dummy Agentic Project",
|
|
24
|
-
"exe": "C:\\Program Files\\Python310\\python.exe",
|
|
25
|
-
"user": "LAPTOP-3FU0HLB5\\Varun"
|
|
26
|
-
},
|
|
27
|
-
"prev_hash": "0000000000000000000000000000000000000000000000000000000000000000",
|
|
28
|
-
"hash": "047618168c7d6a803b7b76e778b6fd05135ad6c81e469cc00588221e9ad21e57"
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
"seq": 1,
|
|
32
|
-
"ts": 1783604464262,
|
|
33
|
-
"kind": "NET_CONNECT",
|
|
34
|
-
"pid": 40008,
|
|
35
|
-
"ppid": null,
|
|
36
|
-
"payload": {
|
|
37
|
-
"bytes": 0,
|
|
38
|
-
"domain": "pt-in-f153.1e100.net",
|
|
39
|
-
"dst_ip": "2404:6800:4000:100c::99",
|
|
40
|
-
"port": 443,
|
|
41
|
-
"proto": "tcp"
|
|
42
|
-
},
|
|
43
|
-
"prev_hash": "047618168c7d6a803b7b76e778b6fd05135ad6c81e469cc00588221e9ad21e57",
|
|
44
|
-
"hash": "ed20e99732dc71fbe7128414b9b66d0de07dcc5c3abaf20dbee7f7d394fe1718"
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
"seq": 2,
|
|
48
|
-
"ts": 1783604464262,
|
|
49
|
-
"kind": "ALERT",
|
|
50
|
-
"pid": null,
|
|
51
|
-
"ppid": null,
|
|
52
|
-
"payload": {
|
|
53
|
-
"message": "Connection to new domain",
|
|
54
|
-
"rule_id": "unknown_domain",
|
|
55
|
-
"severity": "warn"
|
|
56
|
-
},
|
|
57
|
-
"prev_hash": "ed20e99732dc71fbe7128414b9b66d0de07dcc5c3abaf20dbee7f7d394fe1718",
|
|
58
|
-
"hash": "ae82f1646e5391bc13d7bd49fd00bae8096cf2fa29368f566c79a17cea7cd89e"
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
"seq": 3,
|
|
62
|
-
"ts": 1783604464305,
|
|
63
|
-
"kind": "FILE_WRITE",
|
|
64
|
-
"pid": null,
|
|
65
|
-
"ppid": null,
|
|
66
|
-
"payload": {
|
|
67
|
-
"bytes_delta": 46,
|
|
68
|
-
"op": "modify",
|
|
69
|
-
"path": "C:\\Users\\Varun\\Desktop\\Main\\Personal Learning\\Handcuff\\Dummy Agentic Project\\agent_output.txt"
|
|
70
|
-
},
|
|
71
|
-
"prev_hash": "ae82f1646e5391bc13d7bd49fd00bae8096cf2fa29368f566c79a17cea7cd89e",
|
|
72
|
-
"hash": "75f7802b56da702651385cfc59f44ba547ecfdce2b9508cb90ec7ed8b81153ff"
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
"seq": 4,
|
|
76
|
-
"ts": 1783604464305,
|
|
77
|
-
"kind": "FILE_WRITE",
|
|
78
|
-
"pid": null,
|
|
79
|
-
"ppid": null,
|
|
80
|
-
"payload": {
|
|
81
|
-
"bytes_delta": 46,
|
|
82
|
-
"op": "modify",
|
|
83
|
-
"path": "C:\\Users\\Varun\\Desktop\\Main\\Personal Learning\\Handcuff\\Dummy Agentic Project\\agent_output.txt"
|
|
84
|
-
},
|
|
85
|
-
"prev_hash": "75f7802b56da702651385cfc59f44ba547ecfdce2b9508cb90ec7ed8b81153ff",
|
|
86
|
-
"hash": "8b4f21a0f221a029643d1e516be579103205ae3f5e4cd08ddeb71f9b86333bb1"
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
"seq": 5,
|
|
90
|
-
"ts": 1783604464472,
|
|
91
|
-
"kind": "PROCESS_EXIT",
|
|
92
|
-
"pid": 40008,
|
|
93
|
-
"ppid": 40008,
|
|
94
|
-
"payload": {
|
|
95
|
-
"duration_ms": 1085,
|
|
96
|
-
"exit_code": null
|
|
97
|
-
},
|
|
98
|
-
"prev_hash": "8b4f21a0f221a029643d1e516be579103205ae3f5e4cd08ddeb71f9b86333bb1",
|
|
99
|
-
"hash": "7227928e46db3fc85b92f642df1e8f255033769d4949a2bbe99baab11ded5924"
|
|
100
|
-
}
|
|
101
|
-
],
|
|
102
|
-
"alerts": [
|
|
103
|
-
{
|
|
104
|
-
"seq": 1,
|
|
105
|
-
"rule_id": "unknown_domain",
|
|
106
|
-
"severity": "warn",
|
|
107
|
-
"message": "Connection to new domain",
|
|
108
|
-
"muted": false,
|
|
109
|
-
"ts": 1783604464262
|
|
110
|
-
}
|
|
111
|
-
],
|
|
112
|
-
"checkpoints": [],
|
|
113
|
-
"verification": {
|
|
114
|
-
"head_hash": "7227928e46db3fc85b92f642df1e8f255033769d4949a2bbe99baab11ded5924",
|
|
115
|
-
"pubkey": "7cf9d095c6d73e03e4c6dbba67b5905f9787a9705eb85068b46899642e4a8f9f",
|
|
116
|
-
"signature": "c4f1fd20bc4716fe00e47a24ded6bf86f2123a7a8b889b5d25c076917bbd87eec92221a85694761eadca19bd5a013f5e857e174485539bfc70e0847821ac4102"
|
|
117
|
-
}
|
|
118
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# Dummy Agentic Project
|
|
2
|
-
|
|
3
|
-
This is a sample project to demonstrate how to test the `handcuff` monitoring tool.
|
|
4
|
-
It uses LangGraph to orchestrate a workflow with two agents:
|
|
5
|
-
1. **Researcher Agent**: Fetches a random joke from an external API (Network Activity).
|
|
6
|
-
2. **Writer Agent**: Saves the fetched joke to a local file (File Activity).
|
|
7
|
-
|
|
8
|
-
## Setup
|
|
9
|
-
1. Create a virtual environment (optional but recommended):
|
|
10
|
-
```bash
|
|
11
|
-
python -m venv .venv
|
|
12
|
-
.venv\Scripts\activate
|
|
13
|
-
```
|
|
14
|
-
2. Install dependencies:
|
|
15
|
-
```bash
|
|
16
|
-
pip install -r requirements.txt
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## Running the Agent normally
|
|
20
|
-
```bash
|
|
21
|
-
python main.py
|
|
22
|
-
```
|
|
23
|
-
This will fetch a joke and write it to `agent_output.txt`.
|
|
24
|
-
|
|
25
|
-
## Testing with Handcuff
|
|
26
|
-
To verify that Handcuff successfully intercepts the network and file I/O operations, run the script wrapped with the `handcuff` CLI:
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
handcuff watch python main.py
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
### What to look for in Handcuff:
|
|
33
|
-
- **Network Request**: Handcuff should flag the `GET` request to `official-joke-api.appspot.com`.
|
|
34
|
-
- **File Activity**: Handcuff should flag the creation and write operations for `agent_output.txt`.
|
|
35
|
-
|
|
36
|
-
You can view the generated reports in the terminal UI or export them to see the exact logs that Handcuff recorded.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
What do you call a pile of cats? - A Meowtain.
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import httpx
|
|
2
|
-
from typing import TypedDict
|
|
3
|
-
from langgraph.graph import StateGraph, END
|
|
4
|
-
|
|
5
|
-
# Define our state
|
|
6
|
-
class AgentState(TypedDict):
|
|
7
|
-
task: str
|
|
8
|
-
fetched_data: str
|
|
9
|
-
output_file: str
|
|
10
|
-
status: str
|
|
11
|
-
|
|
12
|
-
# Node 1: The Researcher Agent
|
|
13
|
-
# This agent's job is to fetch data from an external API
|
|
14
|
-
def researcher_agent(state: AgentState):
|
|
15
|
-
print(f"[Researcher Agent] Fetching data for task: {state['task']}")
|
|
16
|
-
|
|
17
|
-
# Simulating a web search or API call (Network Activity)
|
|
18
|
-
# We fetch a random joke to have some dynamic content
|
|
19
|
-
response = httpx.get("https://official-joke-api.appspot.com/random_joke")
|
|
20
|
-
if response.status_code == 200:
|
|
21
|
-
data = response.json()
|
|
22
|
-
joke = f"{data['setup']} - {data['punchline']}"
|
|
23
|
-
else:
|
|
24
|
-
joke = "Failed to fetch a joke."
|
|
25
|
-
|
|
26
|
-
print(f"[Researcher Agent] Fetched data: {joke}")
|
|
27
|
-
|
|
28
|
-
return {"fetched_data": joke}
|
|
29
|
-
|
|
30
|
-
# Node 2: The Writer Agent
|
|
31
|
-
# This agent's job is to save the data to a local file
|
|
32
|
-
def writer_agent(state: AgentState):
|
|
33
|
-
print(f"[Writer Agent] Writing data to file: {state['output_file']}")
|
|
34
|
-
|
|
35
|
-
# Simulating file modifications (File Activity)
|
|
36
|
-
with open(state["output_file"], "w", encoding="utf-8") as f:
|
|
37
|
-
f.write(state["fetched_data"])
|
|
38
|
-
|
|
39
|
-
print("[Writer Agent] Data successfully written.")
|
|
40
|
-
return {"status": "completed"}
|
|
41
|
-
|
|
42
|
-
# Build the Graph
|
|
43
|
-
workflow = StateGraph(AgentState)
|
|
44
|
-
|
|
45
|
-
# Add nodes
|
|
46
|
-
workflow.add_node("researcher", researcher_agent)
|
|
47
|
-
workflow.add_node("writer", writer_agent)
|
|
48
|
-
|
|
49
|
-
# Add edges (Simple sequential flow)
|
|
50
|
-
workflow.set_entry_point("researcher")
|
|
51
|
-
workflow.add_edge("researcher", "writer")
|
|
52
|
-
workflow.add_edge("writer", END)
|
|
53
|
-
|
|
54
|
-
# Compile the graph
|
|
55
|
-
app = workflow.compile()
|
|
56
|
-
|
|
57
|
-
if __name__ == "__main__":
|
|
58
|
-
print("=== Starting LangGraph Agentic Workflow ===")
|
|
59
|
-
|
|
60
|
-
initial_state = {
|
|
61
|
-
"task": "Find a joke",
|
|
62
|
-
"fetched_data": "",
|
|
63
|
-
"output_file": "agent_output.txt",
|
|
64
|
-
"status": "pending"
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
# Run the graph
|
|
68
|
-
app.invoke(initial_state)
|
|
69
|
-
|
|
70
|
-
print("=== Workflow Completed ===")
|
|
71
|
-
print("Check 'agent_output.txt' to see the result.")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|