fableforge-agent 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.
- fableforge_agent/__init__.py +63 -0
- fableforge_agent/cli.py +184 -0
- fableforge_agent/control_plane/__init__.py +37 -0
- fableforge_agent/control_plane/auth.py +152 -0
- fableforge_agent/control_plane/core.py +303 -0
- fableforge_agent/observability/__init__.py +29 -0
- fableforge_agent/observability/core.py +229 -0
- fableforge_agent/trust/__init__.py +27 -0
- fableforge_agent/trust/core.py +190 -0
- fableforge_agent-0.1.0.dist-info/METADATA +109 -0
- fableforge_agent-0.1.0.dist-info/RECORD +14 -0
- fableforge_agent-0.1.0.dist-info/WHEEL +5 -0
- fableforge_agent-0.1.0.dist-info/entry_points.txt +2 -0
- fableforge_agent-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Control Plane core — sandboxed execution, audit trails, and incident response.
|
|
3
|
+
|
|
4
|
+
Author: KingLabsA
|
|
5
|
+
- GitHub: https://github.com/KingLabsA
|
|
6
|
+
- Twitter: https://x.com/KingLabsA
|
|
7
|
+
- Email: kinglabsa@users.noreply.github.com
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
import tempfile
|
|
15
|
+
import time
|
|
16
|
+
import uuid
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from datetime import datetime, timezone
|
|
19
|
+
from typing import Dict, List, Optional
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
23
|
+
# Sandbox
|
|
24
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class SandboxPolicy:
|
|
28
|
+
allow_network: bool = False
|
|
29
|
+
allowed_domains: List[str] = field(default_factory=list)
|
|
30
|
+
allowed_paths: List[str] = field(default_factory=list)
|
|
31
|
+
max_files: int = 100
|
|
32
|
+
max_memory_mb: int = 512
|
|
33
|
+
max_cpu_seconds: int = 60
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class SandboxResult:
|
|
38
|
+
stdout: str = ""
|
|
39
|
+
stderr: str = ""
|
|
40
|
+
exit_code: int = 0
|
|
41
|
+
duration_ms: int = 0
|
|
42
|
+
violation: Optional[str] = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Sandbox:
|
|
46
|
+
"""Isolated execution environment for agent code and tool calls.
|
|
47
|
+
|
|
48
|
+
Supported backends: docker, subprocess (Linux seccomp), mock.
|
|
49
|
+
Falls back gracefully when a backend is unavailable.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, sandbox_type: str = "docker"):
|
|
53
|
+
self.sandbox_type = sandbox_type
|
|
54
|
+
self.policy = SandboxPolicy()
|
|
55
|
+
|
|
56
|
+
def _resolve_backend(self) -> str:
|
|
57
|
+
backend = self.sandbox_type
|
|
58
|
+
if backend == "docker":
|
|
59
|
+
try:
|
|
60
|
+
subprocess.run(
|
|
61
|
+
["docker", "version"],
|
|
62
|
+
capture_output=True, timeout=5,
|
|
63
|
+
)
|
|
64
|
+
return "docker"
|
|
65
|
+
except Exception:
|
|
66
|
+
pass
|
|
67
|
+
if backend == "subprocess" or (backend == "docker" and sys.platform == "linux"):
|
|
68
|
+
return "subprocess"
|
|
69
|
+
return "mock"
|
|
70
|
+
|
|
71
|
+
def execute(self, code: str, timeout: int = 30) -> SandboxResult:
|
|
72
|
+
backend = self._resolve_backend()
|
|
73
|
+
start = time.monotonic()
|
|
74
|
+
|
|
75
|
+
if backend == "docker":
|
|
76
|
+
return self._exec_docker(code, timeout)
|
|
77
|
+
elif backend == "subprocess":
|
|
78
|
+
return self._exec_subprocess(code, timeout)
|
|
79
|
+
return self._exec_mock(code, timeout)
|
|
80
|
+
|
|
81
|
+
def _exec_docker(self, code: str, timeout: int) -> SandboxResult:
|
|
82
|
+
try:
|
|
83
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
84
|
+
f.write(code)
|
|
85
|
+
script_path = f.name
|
|
86
|
+
container = f"fableforge-sandbox-{uuid.uuid4().hex[:8]}"
|
|
87
|
+
mount = f"{script_path}:/tmp/script.py:ro"
|
|
88
|
+
proc = subprocess.run(
|
|
89
|
+
[
|
|
90
|
+
"docker", "run", "--rm", "--name", container,
|
|
91
|
+
"-v", mount,
|
|
92
|
+
"--network", "none" if not self.policy.allow_network else "bridge",
|
|
93
|
+
"--memory", f"{self.policy.max_memory_mb}m",
|
|
94
|
+
"--cpus", "1",
|
|
95
|
+
"python:3.11-slim", "python", "/tmp/script.py",
|
|
96
|
+
],
|
|
97
|
+
capture_output=True, text=True, timeout=timeout,
|
|
98
|
+
)
|
|
99
|
+
os.unlink(script_path)
|
|
100
|
+
dur = int((time.monotonic() - start) * 1000)
|
|
101
|
+
return SandboxResult(
|
|
102
|
+
stdout=proc.stdout, stderr=proc.stderr,
|
|
103
|
+
exit_code=proc.returncode, duration_ms=dur,
|
|
104
|
+
)
|
|
105
|
+
except subprocess.TimeoutExpired:
|
|
106
|
+
dur = int((time.monotonic() - start) * 1000)
|
|
107
|
+
return SandboxResult(stderr="TIMEOUT", exit_code=-1, duration_ms=dur,
|
|
108
|
+
violation="resource_exhaustion")
|
|
109
|
+
except Exception as e:
|
|
110
|
+
dur = int((time.monotonic() - start) * 1000)
|
|
111
|
+
return SandboxResult(stderr=str(e), exit_code=-1, duration_ms=dur)
|
|
112
|
+
|
|
113
|
+
def _exec_subprocess(self, code: str, timeout: int) -> SandboxResult:
|
|
114
|
+
start = time.monotonic()
|
|
115
|
+
try:
|
|
116
|
+
proc = subprocess.run(
|
|
117
|
+
[sys.executable, "-c", code],
|
|
118
|
+
capture_output=True, text=True, timeout=timeout,
|
|
119
|
+
)
|
|
120
|
+
dur = int((time.monotonic() - start) * 1000)
|
|
121
|
+
return SandboxResult(
|
|
122
|
+
stdout=proc.stdout, stderr=proc.stderr,
|
|
123
|
+
exit_code=proc.returncode, duration_ms=dur,
|
|
124
|
+
)
|
|
125
|
+
except subprocess.TimeoutExpired:
|
|
126
|
+
dur = int((time.monotonic() - start) * 1000)
|
|
127
|
+
return SandboxResult(stderr="TIMEOUT", exit_code=-1, duration_ms=dur,
|
|
128
|
+
violation="resource_exhaustion")
|
|
129
|
+
except Exception as e:
|
|
130
|
+
dur = int((time.monotonic() - start) * 1000)
|
|
131
|
+
return SandboxResult(stderr=str(e), exit_code=-1, duration_ms=dur)
|
|
132
|
+
|
|
133
|
+
def _exec_mock(self, code: str, timeout: int) -> SandboxResult:
|
|
134
|
+
start = time.monotonic()
|
|
135
|
+
dur = int((time.monotonic() - start) * 1000)
|
|
136
|
+
return SandboxResult(
|
|
137
|
+
stdout=f"[mock] would execute {len(code)} chars",
|
|
138
|
+
exit_code=0, duration_ms=dur,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def file_read(self, path: str) -> str:
|
|
142
|
+
resolved = os.path.abspath(path)
|
|
143
|
+
if not self._path_allowed(resolved):
|
|
144
|
+
raise PermissionError(f"Read denied: {path}")
|
|
145
|
+
with open(resolved) as f:
|
|
146
|
+
return f.read()
|
|
147
|
+
|
|
148
|
+
def file_write(self, path: str, content: str) -> None:
|
|
149
|
+
resolved = os.path.abspath(path)
|
|
150
|
+
if not self._path_allowed(resolved):
|
|
151
|
+
raise PermissionError(f"Write denied: {path}")
|
|
152
|
+
with open(resolved, "w") as f:
|
|
153
|
+
f.write(content)
|
|
154
|
+
|
|
155
|
+
def network_access(self, host: str, port: int) -> bool:
|
|
156
|
+
if not self.policy.allow_network:
|
|
157
|
+
return False
|
|
158
|
+
if self.policy.allowed_domains:
|
|
159
|
+
return any(host.endswith(d) for d in self.policy.allowed_domains)
|
|
160
|
+
return True
|
|
161
|
+
|
|
162
|
+
def _path_allowed(self, resolved: str) -> bool:
|
|
163
|
+
if not self.policy.allowed_paths:
|
|
164
|
+
return True
|
|
165
|
+
return any(resolved.startswith(os.path.abspath(p)) for p in self.policy.allowed_paths)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
169
|
+
# AuditTrail
|
|
170
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
171
|
+
|
|
172
|
+
@dataclass
|
|
173
|
+
class AuditEntry:
|
|
174
|
+
id: str
|
|
175
|
+
timestamp: str
|
|
176
|
+
agent_id: str
|
|
177
|
+
action_type: str
|
|
178
|
+
details: Dict
|
|
179
|
+
severity: str
|
|
180
|
+
signature: Optional[str] = None
|
|
181
|
+
verified: bool = False
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class AuditTrail:
|
|
185
|
+
"""Records every agent action for non-repudiation and forensics."""
|
|
186
|
+
|
|
187
|
+
def __init__(self, agent_id: str = "default"):
|
|
188
|
+
self.agent_id = agent_id
|
|
189
|
+
self._entries: Dict[str, AuditEntry] = {}
|
|
190
|
+
|
|
191
|
+
def record(self, action_type: str, details: Dict, severity: str = "info") -> AuditEntry:
|
|
192
|
+
entry = AuditEntry(
|
|
193
|
+
id=uuid.uuid4().hex,
|
|
194
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
195
|
+
agent_id=self.agent_id,
|
|
196
|
+
action_type=action_type,
|
|
197
|
+
details=details,
|
|
198
|
+
severity=severity,
|
|
199
|
+
)
|
|
200
|
+
self._entries[entry.id] = entry
|
|
201
|
+
return entry
|
|
202
|
+
|
|
203
|
+
def export(self, fmt: str = "json") -> str:
|
|
204
|
+
if fmt == "markdown":
|
|
205
|
+
lines = ["# Audit Trail\n"]
|
|
206
|
+
for e in sorted(self._entries.values(), key=lambda x: x.timestamp):
|
|
207
|
+
lines.append(f"## {e.action_type} ({e.severity})\n")
|
|
208
|
+
lines.append(f"- **ID:** {e.id}\n")
|
|
209
|
+
lines.append(f"- **Time:** {e.timestamp}\n")
|
|
210
|
+
lines.append(f"- **Agent:** {e.agent_id}\n")
|
|
211
|
+
lines.append(f"- **Details:** `{json.dumps(e.details)}`\n")
|
|
212
|
+
if e.signature:
|
|
213
|
+
lines.append(f"- **Signature:** `{e.signature[:32]}...`\n")
|
|
214
|
+
lines.append("\n")
|
|
215
|
+
return "".join(lines)
|
|
216
|
+
return json.dumps(
|
|
217
|
+
[e.__dict__ for e in sorted(self._entries.values(), key=lambda x: x.timestamp)],
|
|
218
|
+
indent=2,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
def search(self, query: str) -> List[AuditEntry]:
|
|
222
|
+
q = query.lower()
|
|
223
|
+
return [
|
|
224
|
+
e for e in self._entries.values()
|
|
225
|
+
if q in e.action_type.lower()
|
|
226
|
+
or q in e.severity.lower()
|
|
227
|
+
or q in json.dumps(e.details).lower()
|
|
228
|
+
]
|
|
229
|
+
|
|
230
|
+
def sign(self, entry_id: str) -> str:
|
|
231
|
+
entry = self._entries.get(entry_id)
|
|
232
|
+
if entry is None:
|
|
233
|
+
raise KeyError(f"No entry: {entry_id}")
|
|
234
|
+
try:
|
|
235
|
+
from cryptography.hazmat.primitives import hashes
|
|
236
|
+
from cryptography.hazmat.primitives.asymmetric import rsa, padding
|
|
237
|
+
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
|
238
|
+
data = json.dumps(entry.__dict__, sort_keys=True).encode()
|
|
239
|
+
sig = private_key.sign(data, padding.PKCS1v15(), hashes.SHA256())
|
|
240
|
+
entry.signature = sig.hex()
|
|
241
|
+
entry.verified = True
|
|
242
|
+
except ImportError:
|
|
243
|
+
entry.signature = f"unsigned-{uuid.uuid4().hex}"
|
|
244
|
+
entry.verified = False
|
|
245
|
+
return entry.signature
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
249
|
+
# IncidentResponse
|
|
250
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
251
|
+
|
|
252
|
+
_INCIDENT_PATTERNS = {
|
|
253
|
+
"data_exfiltration": ["curl", "wget", "nc ", "netcat", "ssh ", "scp "],
|
|
254
|
+
"prompt_injection": ["ignore previous", "ignore all", "system prompt:"],
|
|
255
|
+
"privilege_escalation": ["sudo", "chmod 777", "chown", "setuid"],
|
|
256
|
+
"resource_exhaustion": ["fork bomb", ":(){:|:&}", "while true"],
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class IncidentResponse:
|
|
261
|
+
"""Detects and responds to security incidents in agent behavior."""
|
|
262
|
+
|
|
263
|
+
def detect(self, entry: AuditEntry) -> Optional[str]:
|
|
264
|
+
if entry.action_type == "tool_call":
|
|
265
|
+
args_str = json.dumps(entry.details).lower()
|
|
266
|
+
for incident_type, patterns in _INCIDENT_PATTERNS.items():
|
|
267
|
+
for pat in patterns:
|
|
268
|
+
if pat in args_str:
|
|
269
|
+
return incident_type
|
|
270
|
+
if entry.severity == "critical":
|
|
271
|
+
if entry.details.get("bytes_sent", 0) > 1_000_000:
|
|
272
|
+
return "data_exfiltration"
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
def respond(self, incident_type: str, entry: AuditEntry) -> str:
|
|
276
|
+
actions = {
|
|
277
|
+
"data_exfiltration": f"Blocked outbound transfer from {entry.agent_id}. "
|
|
278
|
+
"Network access revoked for this session.",
|
|
279
|
+
"prompt_injection": f"Prompt injection detected in entry {entry.id[:8]}. "
|
|
280
|
+
"Message quarantined.",
|
|
281
|
+
"privilege_escalation": f"Privilege escalation attempt by {entry.agent_id}. "
|
|
282
|
+
"Sandbox policy tightened.",
|
|
283
|
+
"resource_exhaustion": f"Resource exhaustion from {entry.agent_id}. "
|
|
284
|
+
"Process terminated.",
|
|
285
|
+
}
|
|
286
|
+
return actions.get(incident_type, f"Unknown incident: {incident_type}")
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
290
|
+
# ControlPlane (aggregate)
|
|
291
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
292
|
+
|
|
293
|
+
class ControlPlane:
|
|
294
|
+
"""Top-level agent control plane combining sandbox, audit, and incident response.
|
|
295
|
+
|
|
296
|
+
This is the primary entry point for Tier 3 functionality.
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
def __init__(self, agent_id: str = "default", sandbox_type: str = "docker"):
|
|
300
|
+
self.agent_id = agent_id
|
|
301
|
+
self.sandbox = Sandbox(sandbox_type=sandbox_type)
|
|
302
|
+
self.audit = AuditTrail(agent_id=agent_id)
|
|
303
|
+
self.incidents = IncidentResponse()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Behavioral Observability — Tier 4 of FableForge Agent.
|
|
3
|
+
|
|
4
|
+
Real-time agent behavior observation, semantic firewall, safety rules, and
|
|
5
|
+
behavior reporting.
|
|
6
|
+
|
|
7
|
+
Author: KingLabsA
|
|
8
|
+
- GitHub: https://github.com/KingLabsA
|
|
9
|
+
- Twitter: https://x.com/KingLabsA
|
|
10
|
+
- Email: kinglabsa@users.noreply.github.com
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from fableforge_agent.observability.core import (
|
|
14
|
+
BehavioralObservability,
|
|
15
|
+
Observation,
|
|
16
|
+
SemanticFirewall,
|
|
17
|
+
FirewallDecision,
|
|
18
|
+
SafetyRule,
|
|
19
|
+
BehaviorReport,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"BehavioralObservability",
|
|
24
|
+
"Observation",
|
|
25
|
+
"SemanticFirewall",
|
|
26
|
+
"FirewallDecision",
|
|
27
|
+
"SafetyRule",
|
|
28
|
+
"BehaviorReport",
|
|
29
|
+
]
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Behavioral Observability core — real-time agent observation, semantic firewall,
|
|
3
|
+
safety rules, and behavior reporting.
|
|
4
|
+
|
|
5
|
+
Author: KingLabsA
|
|
6
|
+
- GitHub: https://github.com/KingLabsA
|
|
7
|
+
- Twitter: https://x.com/KingLabsA
|
|
8
|
+
- Email: kinglabsa@users.noreply.github.com
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
import re
|
|
14
|
+
import time
|
|
15
|
+
import uuid
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from typing import Dict, List, Optional, Generator
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
22
|
+
# Safety Rule & Firewall Decision
|
|
23
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
@dataclass
|
|
27
|
+
class SafetyRule:
|
|
28
|
+
name: str
|
|
29
|
+
pattern: str
|
|
30
|
+
action: str # "block", "warn", "log"
|
|
31
|
+
description: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class FirewallDecision:
|
|
36
|
+
allowed: bool
|
|
37
|
+
reason: str
|
|
38
|
+
severity: str # "pass", "warn", "block"
|
|
39
|
+
matched_rule: Optional[str] = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
43
|
+
# Observation & BehaviorReport
|
|
44
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class Observation:
|
|
48
|
+
timestamp: str
|
|
49
|
+
agent_id: str
|
|
50
|
+
action_type: str
|
|
51
|
+
input_hash: str
|
|
52
|
+
output_hash: str
|
|
53
|
+
latency_ms: float
|
|
54
|
+
tool_calls: List[Dict] = field(default_factory=list)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class BehaviorReport:
|
|
58
|
+
"""Aggregated behavior analysis with safety, helpfulness, honesty, and
|
|
59
|
+
efficiency scores."""
|
|
60
|
+
|
|
61
|
+
def __init__(self, observations: Optional[List[Observation]] = None):
|
|
62
|
+
self.observations: List[Observation] = observations or []
|
|
63
|
+
self.violations: List[Dict] = []
|
|
64
|
+
self.scores: Dict[str, float] = {
|
|
65
|
+
"safety": 1.0,
|
|
66
|
+
"helpfulness": 1.0,
|
|
67
|
+
"honesty": 1.0,
|
|
68
|
+
"efficiency": 1.0,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
def summary(self) -> str:
|
|
72
|
+
lines = ["╔═══════════════════════════════════════╗"]
|
|
73
|
+
lines.append("║ Behavior Report Summary ║")
|
|
74
|
+
lines.append("╠═══════════════════════════════════════╣")
|
|
75
|
+
for metric, score in self.scores.items():
|
|
76
|
+
bar = "█" * int(score * 20)
|
|
77
|
+
pct = int(score * 100)
|
|
78
|
+
lines.append(f"║ {metric.capitalize():<12} {bar:20} {pct:3}% ║")
|
|
79
|
+
lines.append("╠═══════════════════════════════════════╣")
|
|
80
|
+
lines.append(f"║ Observations: {len(self.observations):>5} ║")
|
|
81
|
+
lines.append(f"║ Violations: {len(self.violations):>5} ║")
|
|
82
|
+
lines.append("╚═══════════════════════════════════════╝")
|
|
83
|
+
if self.violations:
|
|
84
|
+
lines.append("\nViolations:")
|
|
85
|
+
for v in self.violations:
|
|
86
|
+
lines.append(f" - {v.get('type', 'unknown')}: {v.get('description', '')}")
|
|
87
|
+
return "\n".join(lines)
|
|
88
|
+
|
|
89
|
+
def to_json(self) -> Dict:
|
|
90
|
+
return {
|
|
91
|
+
"observations": [o.__dict__ for o in self.observations],
|
|
92
|
+
"violations": self.violations,
|
|
93
|
+
"scores": self.scores,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
98
|
+
# SemanticFirewall
|
|
99
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
100
|
+
|
|
101
|
+
_DEFAULT_RULES = [
|
|
102
|
+
SafetyRule(
|
|
103
|
+
name="system-prompt-override",
|
|
104
|
+
pattern=r"(?i)(ignore (previous|all|above)|system prompt:|you are now)",
|
|
105
|
+
action="block",
|
|
106
|
+
description="Attempts to override system prompt"),
|
|
107
|
+
SafetyRule(
|
|
108
|
+
name="dangerous-code",
|
|
109
|
+
pattern=r"(?i)(rm\s+-rf|shutdown|format|dd\s+if)",
|
|
110
|
+
action="block",
|
|
111
|
+
description="Dangerous system commands"),
|
|
112
|
+
SafetyRule(
|
|
113
|
+
name="credential-exposure",
|
|
114
|
+
pattern=r"(?i)(sk-[a-zA-Z0-9]{20,}|-----BEGIN\s+(RSA|EC|PRIVATE)\s+KEY-----)",
|
|
115
|
+
action="block",
|
|
116
|
+
description="Exposure of API keys or private keys"),
|
|
117
|
+
SafetyRule(
|
|
118
|
+
name="data-exfiltration",
|
|
119
|
+
pattern=r"(?i)(curl\s+https?://[^\s]*\s*\|)|(wget\s+-O\s*-)",
|
|
120
|
+
action="block",
|
|
121
|
+
description="Data exfiltration via pipe to network"),
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class SemanticFirewall:
|
|
126
|
+
"""Validates prompts, responses, and tool calls against safety rules."""
|
|
127
|
+
|
|
128
|
+
def __init__(self, rules: Optional[List[SafetyRule]] = None):
|
|
129
|
+
self.rules: List[SafetyRule] = rules or _DEFAULT_RULES
|
|
130
|
+
|
|
131
|
+
def check_input(self, prompt: str) -> FirewallDecision:
|
|
132
|
+
return self._check(prompt, "input")
|
|
133
|
+
|
|
134
|
+
def check_output(self, response: str) -> FirewallDecision:
|
|
135
|
+
return self._check(response, "output")
|
|
136
|
+
|
|
137
|
+
def check_tool_call(self, tool: str, args: Dict) -> FirewallDecision:
|
|
138
|
+
combined = json.dumps({"tool": tool, "args": args})
|
|
139
|
+
return self._check(combined, "tool_call")
|
|
140
|
+
|
|
141
|
+
def _check(self, content: str, context: str) -> FirewallDecision:
|
|
142
|
+
for rule in self.rules:
|
|
143
|
+
try:
|
|
144
|
+
if re.search(rule.pattern, content):
|
|
145
|
+
severity = "block" if rule.action == "block" else rule.action
|
|
146
|
+
return FirewallDecision(
|
|
147
|
+
allowed=rule.action != "block",
|
|
148
|
+
reason=f"Rule '{rule.name}' matched in {context}: {rule.description}",
|
|
149
|
+
severity=severity,
|
|
150
|
+
matched_rule=rule.name,
|
|
151
|
+
)
|
|
152
|
+
except re.error:
|
|
153
|
+
continue
|
|
154
|
+
return FirewallDecision(allowed=True, reason="", severity="pass")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
158
|
+
# BehavioralObservability
|
|
159
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
160
|
+
|
|
161
|
+
class BehavioralObservability:
|
|
162
|
+
"""Real-time behavioral observability for agents.
|
|
163
|
+
|
|
164
|
+
Observes agent actions, runs analyzers, and exposes a stream of observations
|
|
165
|
+
compatible with websocket dashboards.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
def __init__(self, agent_id: str = "default"):
|
|
169
|
+
self.agent_id = agent_id
|
|
170
|
+
self._observations: List[Observation] = []
|
|
171
|
+
self._firewall = SemanticFirewall()
|
|
172
|
+
self._subscribers: List[callable] = []
|
|
173
|
+
|
|
174
|
+
def observe(self, action: str, context: Dict) -> Observation:
|
|
175
|
+
input_str = json.dumps(context.get("input", ""))
|
|
176
|
+
output_str = json.dumps(context.get("output", ""))
|
|
177
|
+
obs = Observation(
|
|
178
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
179
|
+
agent_id=self.agent_id,
|
|
180
|
+
action_type=action,
|
|
181
|
+
input_hash=hashlib.sha256(input_str.encode()).hexdigest()[:16],
|
|
182
|
+
output_hash=hashlib.sha256(output_str.encode()).hexdigest()[:16],
|
|
183
|
+
latency_ms=context.get("latency_ms", 0.0),
|
|
184
|
+
tool_calls=context.get("tool_calls", []),
|
|
185
|
+
)
|
|
186
|
+
self._observations.append(obs)
|
|
187
|
+
for sub in self._subscribers:
|
|
188
|
+
try:
|
|
189
|
+
sub(obs)
|
|
190
|
+
except Exception:
|
|
191
|
+
pass
|
|
192
|
+
return obs
|
|
193
|
+
|
|
194
|
+
def analyze(self, observations: Optional[List[Observation]] = None) -> BehaviorReport:
|
|
195
|
+
obs_list = observations or self._observations
|
|
196
|
+
report = BehaviorReport(observations=obs_list)
|
|
197
|
+
total = len(obs_list)
|
|
198
|
+
if total == 0:
|
|
199
|
+
return report
|
|
200
|
+
violations = 0
|
|
201
|
+
for o in obs_list:
|
|
202
|
+
dec = self._firewall.check_input(o.input_hash)
|
|
203
|
+
if not dec.allowed:
|
|
204
|
+
violations += 1
|
|
205
|
+
report.violations.append({
|
|
206
|
+
"type": "firewall_block",
|
|
207
|
+
"description": dec.reason,
|
|
208
|
+
"timestamp": o.timestamp,
|
|
209
|
+
})
|
|
210
|
+
safe = 1.0 - (violations / max(total, 1))
|
|
211
|
+
report.scores["safety"] = max(0.0, safe)
|
|
212
|
+
avg_latency = sum(o.latency_ms for o in obs_list) / total
|
|
213
|
+
report.scores["efficiency"] = max(0.0, min(1.0, 1.0 / (1.0 + avg_latency / 1000)))
|
|
214
|
+
report.scores["helpfulness"] = max(0.0, 1.0 - 0.1 * violations)
|
|
215
|
+
return report
|
|
216
|
+
|
|
217
|
+
def stream(self) -> Generator[Observation, None, None]:
|
|
218
|
+
yield from self._observations
|
|
219
|
+
|
|
220
|
+
def subscribe(self, callback: callable) -> None:
|
|
221
|
+
self._subscribers.append(callback)
|
|
222
|
+
|
|
223
|
+
def check_semantic_intent(self, prompt: str, action: str) -> float:
|
|
224
|
+
prompt_words = set(prompt.lower().split())
|
|
225
|
+
action_words = set(action.lower().split())
|
|
226
|
+
if not prompt_words or not action_words:
|
|
227
|
+
return 0.0
|
|
228
|
+
overlap = prompt_words & action_words
|
|
229
|
+
return len(overlap) / max(len(prompt_words), len(action_words))
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Trust Protocols — Tier 4 of FableForge Agent.
|
|
3
|
+
|
|
4
|
+
Bidirectional agent-to-agent trust, identity registry, and permission
|
|
5
|
+
framework.
|
|
6
|
+
|
|
7
|
+
Author: KingLabsA
|
|
8
|
+
- GitHub: https://github.com/KingLabsA
|
|
9
|
+
- Twitter: https://x.com/KingLabsA
|
|
10
|
+
- Email: kinglabsa@users.noreply.github.com
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from fableforge_agent.trust.core import (
|
|
14
|
+
TrustProtocol,
|
|
15
|
+
HandshakeResult,
|
|
16
|
+
IdentityRegistry,
|
|
17
|
+
AgentIdentity,
|
|
18
|
+
PermissionFramework,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"TrustProtocol",
|
|
23
|
+
"HandshakeResult",
|
|
24
|
+
"IdentityRegistry",
|
|
25
|
+
"AgentIdentity",
|
|
26
|
+
"PermissionFramework",
|
|
27
|
+
]
|