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.
@@ -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
+ ]