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,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FableForge Agent — Self-improving AI agent framework with behavioral
|
|
3
|
+
observability and trust protocols.
|
|
4
|
+
|
|
5
|
+
Built on Hermes Agent (NousResearch) with Tiers 3-4: Agent Control Plane,
|
|
6
|
+
Behavioral Observability, and Bidirectional Trust Protocols.
|
|
7
|
+
|
|
8
|
+
Author: KingLabsA
|
|
9
|
+
- GitHub: https://github.com/KingLabsA
|
|
10
|
+
- Twitter: https://x.com/KingLabsA
|
|
11
|
+
- Email: kinglabsa@users.noreply.github.com
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
__version__ = "0.1.0"
|
|
15
|
+
__author__ = "KingLabsA"
|
|
16
|
+
|
|
17
|
+
# ── Re-exported Hermes symbols ──────────────────────────────────────────────
|
|
18
|
+
try:
|
|
19
|
+
from hermes_agent import AIAgent # noqa: F401
|
|
20
|
+
except ImportError:
|
|
21
|
+
try:
|
|
22
|
+
from run_agent import AIAgent # noqa: F401
|
|
23
|
+
except ImportError:
|
|
24
|
+
AIAgent = None # graceful fallback for partial installs
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
from hermes_agent import ConversationLoop # noqa: F401
|
|
28
|
+
except ImportError:
|
|
29
|
+
try:
|
|
30
|
+
from agent.conversation_loop import ConversationLoop # noqa: F401
|
|
31
|
+
except ImportError:
|
|
32
|
+
ConversationLoop = None
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
from hermes_agent import ToolRegistry # noqa: F401
|
|
36
|
+
except ImportError:
|
|
37
|
+
try:
|
|
38
|
+
from tools.registry import ToolRegistry, registry # noqa: F401
|
|
39
|
+
except ImportError:
|
|
40
|
+
ToolRegistry = None
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
from hermes_agent import SessionDB # noqa: F401
|
|
44
|
+
except ImportError:
|
|
45
|
+
try:
|
|
46
|
+
from hermes_state import SessionDB # noqa: F401
|
|
47
|
+
except ImportError:
|
|
48
|
+
SessionDB = None
|
|
49
|
+
|
|
50
|
+
# ── FableForge additions ────────────────────────────────────────────────────
|
|
51
|
+
from fableforge_agent.control_plane.core import ControlPlane
|
|
52
|
+
from fableforge_agent.observability.core import BehavioralObservability
|
|
53
|
+
from fableforge_agent.trust.core import TrustProtocol
|
|
54
|
+
|
|
55
|
+
__all__ = [
|
|
56
|
+
"AIAgent",
|
|
57
|
+
"ConversationLoop",
|
|
58
|
+
"ToolRegistry",
|
|
59
|
+
"SessionDB",
|
|
60
|
+
"ControlPlane",
|
|
61
|
+
"BehavioralObservability",
|
|
62
|
+
"TrustProtocol",
|
|
63
|
+
]
|
fableforge_agent/cli.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
FableForge Agent CLI — wraps Hermes Agent and adds FableForge commands for
|
|
4
|
+
the control plane, observability, trust, and audit.
|
|
5
|
+
|
|
6
|
+
Author: KingLabsA
|
|
7
|
+
- GitHub: https://github.com/KingLabsA
|
|
8
|
+
- Twitter: https://x.com/KingLabsA
|
|
9
|
+
- Email: kinglabsa@users.noreply.github.com
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def cmd_control_plane(args: argparse.Namespace) -> None:
|
|
19
|
+
port = args.port
|
|
20
|
+
host = args.host
|
|
21
|
+
try:
|
|
22
|
+
import uvicorn
|
|
23
|
+
from fastapi import FastAPI
|
|
24
|
+
|
|
25
|
+
app = FastAPI(title="FableForge Control Plane")
|
|
26
|
+
|
|
27
|
+
@app.get("/health")
|
|
28
|
+
def health():
|
|
29
|
+
return {"status": "ok"}
|
|
30
|
+
|
|
31
|
+
@app.get("/audit")
|
|
32
|
+
def get_audit():
|
|
33
|
+
from fableforge_agent.control_plane import AuditTrail
|
|
34
|
+
trail = AuditTrail()
|
|
35
|
+
return {"entries": trail.export("json")}
|
|
36
|
+
|
|
37
|
+
@app.post("/sandbox/execute")
|
|
38
|
+
async def sandbox_execute(body: dict):
|
|
39
|
+
from fableforge_agent.control_plane import Sandbox
|
|
40
|
+
sandbox = Sandbox(sandbox_type=body.get("backend", "mock"))
|
|
41
|
+
result = sandbox.execute(body.get("code", ""), timeout=body.get("timeout", 30))
|
|
42
|
+
return result.__dict__
|
|
43
|
+
|
|
44
|
+
uvicorn.run(app, host=host, port=port, log_level="info")
|
|
45
|
+
except ImportError as e:
|
|
46
|
+
print(f"Error: missing dependency — {e}", file=sys.stderr)
|
|
47
|
+
print("Install with: pip install 'fableforge-agent[server]'", file=sys.stderr)
|
|
48
|
+
sys.exit(1)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def cmd_audit(args: argparse.Namespace) -> None:
|
|
52
|
+
from fableforge_agent.control_plane import AuditTrail
|
|
53
|
+
|
|
54
|
+
trail = AuditTrail()
|
|
55
|
+
export_path = args.export
|
|
56
|
+
fmt = "json" if export_path and export_path.endswith(".json") else "markdown"
|
|
57
|
+
output = trail.export(fmt=fmt)
|
|
58
|
+
if export_path:
|
|
59
|
+
with open(export_path, "w") as f:
|
|
60
|
+
f.write(output)
|
|
61
|
+
print(f"Audit exported to {export_path}")
|
|
62
|
+
else:
|
|
63
|
+
print(output)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def cmd_trust(args: argparse.Namespace) -> None:
|
|
67
|
+
from fableforge_agent.trust import TrustProtocol
|
|
68
|
+
|
|
69
|
+
agent_id = args.agent_id or "cli-agent"
|
|
70
|
+
trust = TrustProtocol(agent_id=agent_id)
|
|
71
|
+
action = args.action
|
|
72
|
+
|
|
73
|
+
if action == "register":
|
|
74
|
+
pubkey = args.pubkey
|
|
75
|
+
if pubkey and os.path.exists(pubkey):
|
|
76
|
+
with open(pubkey) as f:
|
|
77
|
+
key_content = f.read().strip()
|
|
78
|
+
else:
|
|
79
|
+
key_content = pubkey or trust.public_key
|
|
80
|
+
trust.registry.register(agent_id, key_content, {"source": "cli"})
|
|
81
|
+
print(f"Registered agent: {agent_id}")
|
|
82
|
+
print(f" Public key fingerprint: {trust.public_key_fingerprint}")
|
|
83
|
+
elif action == "list":
|
|
84
|
+
agents = trust.registry.list_agents()
|
|
85
|
+
print("Registered agents:")
|
|
86
|
+
for aid in agents:
|
|
87
|
+
identity = trust.registry.resolve(aid)
|
|
88
|
+
print(f" - {aid} (trust level: {identity.trust_level if identity else '?'})")
|
|
89
|
+
elif action == "verify":
|
|
90
|
+
challenge = f"challenge-{agent_id}"
|
|
91
|
+
response = hashlib_response = __import__("hashlib").sha256(
|
|
92
|
+
f"{agent_id}:{challenge}:{trust.public_key}".encode()
|
|
93
|
+
).hexdigest()
|
|
94
|
+
valid = trust.registry.verify_identity(agent_id, challenge, response)
|
|
95
|
+
print(f"Identity verification: {'PASS' if valid else 'FAIL'}")
|
|
96
|
+
else:
|
|
97
|
+
print(f"Unknown trust action: {action}")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def cmd_observe(args: argparse.Namespace) -> None:
|
|
101
|
+
try:
|
|
102
|
+
import uvicorn
|
|
103
|
+
from fastapi import FastAPI, WebSocket
|
|
104
|
+
|
|
105
|
+
app = FastAPI(title="FableForge Observability")
|
|
106
|
+
|
|
107
|
+
@app.websocket("/ws/observe")
|
|
108
|
+
async def observe_ws(websocket: WebSocket):
|
|
109
|
+
await websocket.accept()
|
|
110
|
+
from fableforge_agent.observability import BehavioralObservability
|
|
111
|
+
obs = BehavioralObservability(agent_id=args.agent_id or "observed")
|
|
112
|
+
while True:
|
|
113
|
+
data = await websocket.receive_json()
|
|
114
|
+
observation = obs.observe(data.get("action", "unknown"), data)
|
|
115
|
+
await websocket.send_json(observation.__dict__)
|
|
116
|
+
|
|
117
|
+
uvicorn.run(app, host=args.host, port=args.port, log_level="info")
|
|
118
|
+
except ImportError as e:
|
|
119
|
+
print(f"Error: missing dependency — {e}", file=sys.stderr)
|
|
120
|
+
sys.exit(1)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def main() -> None:
|
|
124
|
+
parser = argparse.ArgumentParser(
|
|
125
|
+
prog="fableforge",
|
|
126
|
+
description="FableForge Agent — self-improving AI agent with behavioral observability and trust",
|
|
127
|
+
epilog="Author: KingLabsA — https://github.com/KingLabsA",
|
|
128
|
+
)
|
|
129
|
+
parser.add_argument("--version", action="version", version="0.1.0")
|
|
130
|
+
|
|
131
|
+
subparsers = parser.add_subparsers(dest="command", help="Subcommands")
|
|
132
|
+
|
|
133
|
+
# control-plane
|
|
134
|
+
cp_parser = subparsers.add_parser("control-plane", help="Start the control plane API server")
|
|
135
|
+
cp_parser.add_argument("--host", default="127.0.0.1")
|
|
136
|
+
cp_parser.add_argument("--port", type=int, default=8000)
|
|
137
|
+
|
|
138
|
+
# audit
|
|
139
|
+
audit_parser = subparsers.add_parser("audit", help="Export audit trail")
|
|
140
|
+
audit_parser.add_argument("--export", dest="export_path", help="Export to file (.json or .md)")
|
|
141
|
+
|
|
142
|
+
# trust
|
|
143
|
+
trust_parser = subparsers.add_parser("trust", help="Trust registry management")
|
|
144
|
+
trust_sub = trust_parser.add_subparsers(dest="action", required=True)
|
|
145
|
+
reg_parser = trust_sub.add_parser("register", help="Register an agent")
|
|
146
|
+
reg_parser.add_argument("--agent-id", required=True)
|
|
147
|
+
reg_parser.add_argument("--pubkey", help="Public key path")
|
|
148
|
+
list_parser = trust_sub.add_parser("list", help="List registered agents")
|
|
149
|
+
list_parser.add_argument("--agent-id", default="cli-agent")
|
|
150
|
+
verify_parser = trust_sub.add_parser("verify", help="Verify identity")
|
|
151
|
+
verify_parser.add_argument("--agent-id", default="cli-agent")
|
|
152
|
+
|
|
153
|
+
# observe
|
|
154
|
+
obs_parser = subparsers.add_parser("observe", help="Start behavioral observability dashboard")
|
|
155
|
+
obs_parser.add_argument("--host", default="127.0.0.1")
|
|
156
|
+
obs_parser.add_argument("--port", type=int, default=8765)
|
|
157
|
+
obs_parser.add_argument("--agent-id", default="observed")
|
|
158
|
+
|
|
159
|
+
args = parser.parse_args()
|
|
160
|
+
|
|
161
|
+
if args.command == "control-plane":
|
|
162
|
+
cmd_control_plane(args)
|
|
163
|
+
elif args.command == "audit":
|
|
164
|
+
cmd_audit(args)
|
|
165
|
+
elif args.command == "trust":
|
|
166
|
+
cmd_trust(args)
|
|
167
|
+
elif args.command == "observe":
|
|
168
|
+
cmd_observe(args)
|
|
169
|
+
else:
|
|
170
|
+
# Default: delegate to Hermes CLI
|
|
171
|
+
try:
|
|
172
|
+
from hermes_cli.cli import main as hermes_main
|
|
173
|
+
hermes_main()
|
|
174
|
+
except ImportError:
|
|
175
|
+
try:
|
|
176
|
+
from cli import main as hermes_main
|
|
177
|
+
hermes_main()
|
|
178
|
+
except ImportError:
|
|
179
|
+
print("Hermes Agent not installed. Install with: pip install hermes-agent", file=sys.stderr)
|
|
180
|
+
sys.exit(1)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
if __name__ == "__main__":
|
|
184
|
+
main()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Control Plane — Tier 3 of FableForge Agent.
|
|
3
|
+
|
|
4
|
+
Sandboxed execution, audit trails, incident response, authentication, and
|
|
5
|
+
credential management for agent workloads.
|
|
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.control_plane.core import (
|
|
14
|
+
ControlPlane,
|
|
15
|
+
Sandbox,
|
|
16
|
+
SandboxPolicy,
|
|
17
|
+
SandboxResult,
|
|
18
|
+
AuditTrail,
|
|
19
|
+
AuditEntry,
|
|
20
|
+
IncidentResponse,
|
|
21
|
+
)
|
|
22
|
+
from fableforge_agent.control_plane.auth import (
|
|
23
|
+
OAuthGate,
|
|
24
|
+
CredentialBroker,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"ControlPlane",
|
|
29
|
+
"Sandbox",
|
|
30
|
+
"SandboxPolicy",
|
|
31
|
+
"SandboxResult",
|
|
32
|
+
"AuditTrail",
|
|
33
|
+
"AuditEntry",
|
|
34
|
+
"IncidentResponse",
|
|
35
|
+
"OAuthGate",
|
|
36
|
+
"CredentialBroker",
|
|
37
|
+
]
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Authentication and credential management for the agent control plane.
|
|
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 time
|
|
13
|
+
import uuid
|
|
14
|
+
from typing import Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
18
|
+
# OAuthGate
|
|
19
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
20
|
+
|
|
21
|
+
class OAuthGate:
|
|
22
|
+
"""Pluggable OAuth authentication and authorization for agent access control."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, secrets_path: Optional[str] = None):
|
|
25
|
+
self._tokens: Dict[str, Dict] = {}
|
|
26
|
+
self._clients: Dict[str, str] = {}
|
|
27
|
+
self.secrets_path = secrets_path or os.path.expanduser("~/.fableforge/secrets.json")
|
|
28
|
+
self._load_secrets()
|
|
29
|
+
|
|
30
|
+
def _load_secrets(self) -> None:
|
|
31
|
+
try:
|
|
32
|
+
with open(self.secrets_path) as f:
|
|
33
|
+
data = json.load(f)
|
|
34
|
+
self._clients = data.get("clients", {})
|
|
35
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
|
36
|
+
self._clients = {}
|
|
37
|
+
|
|
38
|
+
def _save_secrets(self) -> None:
|
|
39
|
+
os.makedirs(os.path.dirname(self.secrets_path), exist_ok=True)
|
|
40
|
+
with open(self.secrets_path, "w") as f:
|
|
41
|
+
json.dump({"clients": self._clients}, f, indent=2)
|
|
42
|
+
|
|
43
|
+
def authenticate(self, credentials: Dict) -> bool:
|
|
44
|
+
client_id = credentials.get("client_id")
|
|
45
|
+
client_secret = credentials.get("client_secret")
|
|
46
|
+
stored = self._clients.get(client_id)
|
|
47
|
+
if stored and stored == client_secret:
|
|
48
|
+
return True
|
|
49
|
+
if not self._clients:
|
|
50
|
+
return True
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
def authorize(self, agent_id: str, action: str) -> bool:
|
|
54
|
+
token = self._tokens.get(agent_id)
|
|
55
|
+
if not token:
|
|
56
|
+
return False
|
|
57
|
+
if token.get("expires_at", 0) < time.time():
|
|
58
|
+
return False
|
|
59
|
+
return action in token.get("scopes", [])
|
|
60
|
+
|
|
61
|
+
def issue_token(self, agent_id: str, scopes: List[str]) -> str:
|
|
62
|
+
token = uuid.uuid4().hex
|
|
63
|
+
self._tokens[agent_id] = {
|
|
64
|
+
"token": token,
|
|
65
|
+
"scopes": scopes,
|
|
66
|
+
"expires_at": time.time() + 3600,
|
|
67
|
+
"issued_at": time.time(),
|
|
68
|
+
}
|
|
69
|
+
return token
|
|
70
|
+
|
|
71
|
+
def verify_token(self, token: str) -> Dict:
|
|
72
|
+
for agent_id, data in self._tokens.items():
|
|
73
|
+
if data.get("token") == token:
|
|
74
|
+
if data["expires_at"] > time.time():
|
|
75
|
+
return {"agent_id": agent_id, "scopes": data["scopes"], "valid": True}
|
|
76
|
+
return {"agent_id": agent_id, "valid": False, "reason": "expired"}
|
|
77
|
+
return {"valid": False, "reason": "unknown_token"}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
81
|
+
# CredentialBroker
|
|
82
|
+
# ═════════════════════════════════════════════════════════════════════════════════
|
|
83
|
+
|
|
84
|
+
class CredentialBroker:
|
|
85
|
+
"""Secure credential management with encrypted storage."""
|
|
86
|
+
|
|
87
|
+
def __init__(self, vault_path: Optional[str] = None):
|
|
88
|
+
self.vault_path = vault_path or os.path.expanduser("~/.fableforge/vault.json")
|
|
89
|
+
self._vault: Dict[str, Dict] = {}
|
|
90
|
+
self._load_vault()
|
|
91
|
+
|
|
92
|
+
def _load_vault(self) -> None:
|
|
93
|
+
try:
|
|
94
|
+
with open(self.vault_path) as f:
|
|
95
|
+
raw = f.read()
|
|
96
|
+
self._vault = json.loads(self._decrypt(raw))
|
|
97
|
+
except (FileNotFoundError, json.JSONDecodeError, Exception):
|
|
98
|
+
self._vault = {}
|
|
99
|
+
|
|
100
|
+
def _save_vault(self) -> None:
|
|
101
|
+
os.makedirs(os.path.dirname(self.vault_path), exist_ok=True)
|
|
102
|
+
with open(self.vault_path, "w") as f:
|
|
103
|
+
f.write(self._encrypt(json.dumps(self._vault, indent=2)))
|
|
104
|
+
|
|
105
|
+
def _encrypt(self, plaintext: str) -> str:
|
|
106
|
+
try:
|
|
107
|
+
from cryptography.fernet import Fernet
|
|
108
|
+
key = self._get_or_create_key()
|
|
109
|
+
return Fernet(key).encrypt(plaintext.encode()).decode()
|
|
110
|
+
except ImportError:
|
|
111
|
+
return plaintext
|
|
112
|
+
|
|
113
|
+
def _decrypt(self, ciphertext: str) -> str:
|
|
114
|
+
try:
|
|
115
|
+
from cryptography.fernet import Fernet
|
|
116
|
+
key = self._get_or_create_key()
|
|
117
|
+
return Fernet(key).decrypt(ciphertext.encode()).decode()
|
|
118
|
+
except (ImportError, Exception):
|
|
119
|
+
return ciphertext
|
|
120
|
+
|
|
121
|
+
def _get_or_create_key(self) -> bytes:
|
|
122
|
+
key_path = os.path.expanduser("~/.fableforge/vault.key")
|
|
123
|
+
try:
|
|
124
|
+
with open(key_path, "rb") as f:
|
|
125
|
+
return f.read()
|
|
126
|
+
except FileNotFoundError:
|
|
127
|
+
try:
|
|
128
|
+
from cryptography.fernet import Fernet
|
|
129
|
+
key = Fernet.generate_key()
|
|
130
|
+
os.makedirs(os.path.dirname(key_path), exist_ok=True)
|
|
131
|
+
with open(key_path, "wb") as f:
|
|
132
|
+
f.write(key)
|
|
133
|
+
return key
|
|
134
|
+
except ImportError:
|
|
135
|
+
return b"insecure-fallback-key-do-not-use-in-production"
|
|
136
|
+
|
|
137
|
+
def store(self, service: str, credential: Dict) -> None:
|
|
138
|
+
self._vault[service] = credential
|
|
139
|
+
self._save_vault()
|
|
140
|
+
|
|
141
|
+
def retrieve(self, service: str) -> Optional[Dict]:
|
|
142
|
+
return self._vault.get(service)
|
|
143
|
+
|
|
144
|
+
def rotate(self, service: str) -> None:
|
|
145
|
+
if service in self._vault:
|
|
146
|
+
old = self._vault[service]
|
|
147
|
+
rotated = {k: f"{k}-rotated-{uuid.uuid4().hex[:8]}" for k in old}
|
|
148
|
+
self._vault[service] = rotated
|
|
149
|
+
self._save_vault()
|
|
150
|
+
|
|
151
|
+
def list_services(self) -> List[str]:
|
|
152
|
+
return list(self._vault.keys())
|