fable-engine 1.3.1__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.
Files changed (104) hide show
  1. fable_compressor.py +356 -0
  2. fable_engine/__init__.py +1 -0
  3. fable_engine/actions/__init__.py +291 -0
  4. fable_engine/actions/cas.py +182 -0
  5. fable_engine/actions/deliberation.py +523 -0
  6. fable_engine/actions/fleet.py +807 -0
  7. fable_engine/actions/lifecycle.py +298 -0
  8. fable_engine/actions/scrapers.py +116 -0
  9. fable_engine/actions/system3.py +815 -0
  10. fable_engine/browser.py +824 -0
  11. fable_engine/cas.py +974 -0
  12. fable_engine/fable_session.json +510 -0
  13. fable_engine/guards.py +283 -0
  14. fable_engine/schema.py +714 -0
  15. fable_engine/scrapers/__init__.py +32 -0
  16. fable_engine/scrapers/arxiv.py +115 -0
  17. fable_engine/scrapers/base.py +386 -0
  18. fable_engine/scrapers/github.py +129 -0
  19. fable_engine/scrapers/reddit.py +154 -0
  20. fable_engine/scrapers/web.py +120 -0
  21. fable_engine/scrapers/x.py +125 -0
  22. fable_engine/scrapers/youtube.py +132 -0
  23. fable_engine/server.py +414 -0
  24. fable_engine/session.py +1819 -0
  25. fable_engine/test_server.py +1362 -0
  26. fable_engine/updater.py +541 -0
  27. fable_engine-1.3.1.dist-info/LICENSE +22 -0
  28. fable_engine-1.3.1.dist-info/METADATA +173 -0
  29. fable_engine-1.3.1.dist-info/RECORD +104 -0
  30. fable_engine-1.3.1.dist-info/WHEEL +5 -0
  31. fable_engine-1.3.1.dist-info/entry_points.txt +5 -0
  32. fable_engine-1.3.1.dist-info/top_level.txt +6 -0
  33. fable_mode/__init__.py +3 -0
  34. fable_mode/__main__.py +4 -0
  35. fable_mode/adapters.py +1014 -0
  36. fable_mode/installer.py +553 -0
  37. fable_mode/launcher.py +437 -0
  38. fable_mode/manifest.py +142 -0
  39. fable_mode/resources.json +114 -0
  40. fable_mode/safety.py +103 -0
  41. fable_mode_entry.py +10 -0
  42. fable_v2/__init__.py +146 -0
  43. fable_v2/adapters.py +151 -0
  44. fable_v2/coder_fleet/__init__.py +100 -0
  45. fable_v2/coder_fleet/ast_tools.py +158 -0
  46. fable_v2/coder_fleet/compute.py +199 -0
  47. fable_v2/coder_fleet/design_engine.py +1316 -0
  48. fable_v2/coder_fleet/diagnostics.py +293 -0
  49. fable_v2/coder_fleet/fleet_dispatcher.py +214 -0
  50. fable_v2/coder_fleet/mock_auditor.py +306 -0
  51. fable_v2/coder_fleet/mutation.py +216 -0
  52. fable_v2/coder_fleet/property_oracle.py +260 -0
  53. fable_v2/coder_fleet/receipt_attestor.py +122 -0
  54. fable_v2/coder_fleet/red_team_swarm.py +908 -0
  55. fable_v2/coder_fleet/test_harness.py +198 -0
  56. fable_v2/coder_fleet/vector_engine.py +1287 -0
  57. fable_v2/coder_fleet/visual.py +357 -0
  58. fable_v2/coder_fleet/workspace.py +153 -0
  59. fable_v2/cortical/__init__.py +20 -0
  60. fable_v2/cortical/plasticity_engine.py +992 -0
  61. fable_v2/execution_broker.py +811 -0
  62. fable_v2/proof_engine.py +1141 -0
  63. fable_v2/protocol.py +485 -0
  64. fable_v2/runtime.py +1010 -0
  65. fable_v2/system3/__init__.py +204 -0
  66. fable_v2/system3/causal.py +558 -0
  67. fable_v2/system3/dialectical.py +577 -0
  68. fable_v2/system3/evolution.py +503 -0
  69. fable_v2/system3/executive.py +338 -0
  70. fable_v2/system3/free_energy.py +479 -0
  71. fable_v2/system3/hyperbolic.py +555 -0
  72. fable_v2/system3/induction.py +336 -0
  73. fable_v2/system3/kripke.py +548 -0
  74. fable_v2/system3/oracle.py +745 -0
  75. fable_v2/verifiers.py +72 -0
  76. tests/__init__.py +1 -0
  77. tests/test_anti_loop_circuit_breaker.py +64 -0
  78. tests/test_auto_updater.py +407 -0
  79. tests/test_coder_fleet.py +535 -0
  80. tests/test_delegation_compiler.py +54 -0
  81. tests/test_descriptor_boundaries.py +126 -0
  82. tests/test_design_engine.py +603 -0
  83. tests/test_epistemic_evidence_validator.py +66 -0
  84. tests/test_execution_broker.py +233 -0
  85. tests/test_fable_v2.py +406 -0
  86. tests/test_fleet_transitions.py +116 -0
  87. tests/test_fsm_redteam_evolution.py +406 -0
  88. tests/test_goal_rubric_and_pipeline.py +367 -0
  89. tests/test_hebbian_plasticity.py +585 -0
  90. tests/test_packaging_runtime.py +194 -0
  91. tests/test_proof_engine.py +259 -0
  92. tests/test_red_team_swarm.py +645 -0
  93. tests/test_redteam_remediation.py +169 -0
  94. tests/test_registration_transaction.py +375 -0
  95. tests/test_requested_regressions.py +467 -0
  96. tests/test_scrapers.py +370 -0
  97. tests/test_server_actions.py +93 -0
  98. tests/test_server_frontier_actions.py +269 -0
  99. tests/test_server_protocol.py +88 -0
  100. tests/test_stealth_browser.py +970 -0
  101. tests/test_system3.py +381 -0
  102. tests/test_system3_deep_integration.py +385 -0
  103. tests/test_system3_frontier.py +436 -0
  104. tests/test_vector_engine.py +608 -0
fable_engine/server.py ADDED
@@ -0,0 +1,414 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Fable-Engine MCP Server for MCP-compatible agent hosts.
4
+ Implements the fable_session tool for deep cognitive session management,
5
+ epistemic tracking, invariant recording, anti-rush lockout enforcement,
6
+ user-controlled time-budgeted pacing telemetry, and session persistence.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import io
12
+ import json
13
+ import logging
14
+ import os
15
+ import sys
16
+ from pathlib import Path
17
+ from typing import Any, Dict
18
+
19
+ # Reconfigure UTF-8 for Windows stdio
20
+ if hasattr(sys.stdout, "reconfigure") and sys.stdout.encoding != "utf-8":
21
+ try:
22
+ sys.stdout.reconfigure(encoding="utf-8")
23
+ except Exception:
24
+ pass
25
+ if hasattr(sys.stdin, "reconfigure") and sys.stdin.encoding != "utf-8":
26
+ try:
27
+ sys.stdin.reconfigure(encoding="utf-8")
28
+ except Exception:
29
+ pass
30
+
31
+ # Configure logging exclusively to stderr so stdout remains pure JSON-RPC
32
+ logging.basicConfig(
33
+ stream=sys.stderr,
34
+ level=logging.INFO,
35
+ format="%(asctime)s [%(levelname)s] [Fable-Engine] %(message)s",
36
+ )
37
+ logger = logging.getLogger("fable-engine")
38
+
39
+ BASE_DIR = Path(__file__).resolve().parent
40
+ if str(BASE_DIR.parent) not in sys.path:
41
+ sys.path.insert(0, str(BASE_DIR.parent))
42
+
43
+ # Re-export all symbols from modular subsystems for 100% backward compatibility
44
+ from fable_engine.cas import (
45
+ DATA_DIR,
46
+ FABLE_CAS_DIR,
47
+ MAX_CAS_OBJECT_BYTES,
48
+ MAX_SLICE_RESPONSE_BYTES,
49
+ AdaptiveChunkAccumulator,
50
+ CASNotFoundError,
51
+ CASSliceViewer,
52
+ CAS_ENGINE,
53
+ CompositeFrame,
54
+ FableCASError,
55
+ FableCASStore,
56
+ FableCompress,
57
+ FableGrammar333,
58
+ IntegrityError,
59
+ ThreadSafeLRUCache,
60
+ _assert_private_path,
61
+ _open_directory_nofollow,
62
+ _safe_cas_node,
63
+ )
64
+ from fable_engine.guards import (
65
+ GLOBAL_VELOCITY_PROFILER,
66
+ AntiLoopCircuitBreaker,
67
+ DelegationContractCompiler,
68
+ EpistemicEvidenceValidator,
69
+ ModelVelocityProfiler,
70
+ )
71
+ from fable_engine.browser import (
72
+ DEFAULT_BROWSER_OPEN_TIMEOUT_SECONDS,
73
+ GLOBAL_BROWSER_ENGINE,
74
+ bound_browser_timeout,
75
+ )
76
+ from fable_engine.schema import BROWSER_TOOL_SCHEMAS, TOOL_SCHEMA
77
+ from fable_engine.session import (
78
+ ACTIVE_SESSIONS,
79
+ FORCE_UNLOCK_ENV,
80
+ MAX_TIME_BUDGET_MINUTES,
81
+ MIN_TIME_BUDGET_MINUTES,
82
+ PHASE_INDEX_MAP,
83
+ PHASES,
84
+ SESSIONS_DIR,
85
+ SESSION_NAME_PATTERN,
86
+ SILENT_DELIBERATION_REMINDER,
87
+ VALID_TRANSITIONS,
88
+ FableSession,
89
+ SessionState,
90
+ _safe_session_file,
91
+ _validate_session_name,
92
+ _validate_time_budget,
93
+ get_or_load_session,
94
+ session_file_lock,
95
+ )
96
+
97
+ def __getattr__(name: str) -> Any:
98
+ if name in ("GLOBAL_PLASTICITY_ENGINE", "GLOBAL_RED_TEAM_SWARM"):
99
+ from fable_engine import session
100
+ return getattr(session, name)
101
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
102
+
103
+ from fable_engine.actions import (
104
+ ACTION_DISPATCH,
105
+ handle_fable_session,
106
+ )
107
+
108
+ try:
109
+ from fable_engine.updater import AutoUpdater
110
+ except ImportError:
111
+ try:
112
+ from updater import AutoUpdater
113
+ except ImportError:
114
+ AutoUpdater = None
115
+
116
+ MAX_RPC_LINE_BYTES = 1 * 1024 * 1024
117
+ MAX_RPC_RESPONSE_BYTES = 2 * 1024 * 1024
118
+ BROWSER_TOOL_NAMES = frozenset(tool["name"] for tool in BROWSER_TOOL_SCHEMAS)
119
+
120
+
121
+ def send_response(response_dict: Dict[str, Any]):
122
+ """Writes a bounded JSON-RPC response to stdout with UTF-8 safety."""
123
+ encoded = json.dumps(response_dict, ensure_ascii=False)
124
+ encoded_bytes = encoded.encode("utf-8")
125
+ if len(encoded_bytes) > MAX_RPC_RESPONSE_BYTES:
126
+ response_dict = {
127
+ "jsonrpc": "2.0",
128
+ "id": response_dict.get("id") if isinstance(response_dict, dict) else None,
129
+ "error": {"code": -32000, "message": "Response exceeds maximum size"},
130
+ }
131
+ encoded = json.dumps(response_dict, ensure_ascii=False)
132
+ encoded_bytes = encoded.encode("utf-8")
133
+ payload = encoded_bytes + b"\n"
134
+ if hasattr(sys.stdout, "buffer") and sys.stdout.buffer is not None:
135
+ try:
136
+ sys.stdout.buffer.write(payload)
137
+ sys.stdout.buffer.flush()
138
+ return
139
+ except Exception:
140
+ pass
141
+ try:
142
+ sys.stdout.write(encoded + "\n")
143
+ sys.stdout.flush()
144
+ except UnicodeEncodeError:
145
+ sys.stdout.write(payload.decode("utf-8", errors="replace"))
146
+ sys.stdout.flush()
147
+
148
+
149
+ def _bounded_lines(stream, limit: int):
150
+ """Yield newline-delimited frames without waiting for EOF or calling stream.readline()."""
151
+ raw_stream = getattr(stream, "buffer", None)
152
+ if raw_stream is None or not hasattr(raw_stream, "read"):
153
+ raw_stream = stream
154
+ read_fn = getattr(raw_stream, "read1", raw_stream.read)
155
+ pending = bytearray()
156
+ oversized = False
157
+ while True:
158
+ chunk = read_fn(4096)
159
+ if not chunk:
160
+ if pending or oversized:
161
+ yield bytes(pending).decode("utf-8", "replace"), oversized
162
+ return
163
+ if isinstance(chunk, str):
164
+ encoded = chunk.encode("utf-8", "replace")
165
+ else:
166
+ encoded = bytes(chunk)
167
+ for byte in encoded:
168
+ if byte == 0x0A:
169
+ yield bytes(pending).decode("utf-8", "replace"), oversized
170
+ pending.clear()
171
+ oversized = False
172
+ elif not oversized:
173
+ pending.append(byte)
174
+ if len(pending) > limit:
175
+ oversized = True
176
+ del pending[limit:]
177
+
178
+
179
+ def main():
180
+ logger.info("Starting Fable-Engine MCP Server on stdio...")
181
+ if (
182
+ AutoUpdater is not None
183
+ and not os.environ.get("FABLE_DISABLE_AUTO_UPDATE")
184
+ and not os.environ.get("PYTEST_CURRENT_TEST")
185
+ and not os.environ.get("GITHUB_ACTIONS")
186
+ and not os.environ.get("CI")
187
+ ):
188
+ try:
189
+ AutoUpdater().trigger_silent_background_update()
190
+ except Exception as e:
191
+ logger.debug(f"Silent auto-updater background trigger failed in main: {e}")
192
+ for line, oversized in _bounded_lines(sys.stdin, MAX_RPC_LINE_BYTES):
193
+ if oversized:
194
+ send_response({"jsonrpc": "2.0", "id": None, "error": {"code": -32600, "message": "Invalid Request"}})
195
+ continue
196
+ # Count the raw frame before trimming whitespace; otherwise an attacker
197
+ # can bypass the line limit with an oversized whitespace prefix/suffix.
198
+ if len(line.encode("utf-8", "replace")) > MAX_RPC_LINE_BYTES:
199
+ send_response({"jsonrpc": "2.0", "id": None, "error": {"code": -32600, "message": "Invalid Request"}})
200
+ continue
201
+ line = line.strip()
202
+ if not line:
203
+ continue
204
+
205
+ if len(line.encode("utf-8", "replace")) > MAX_RPC_LINE_BYTES:
206
+ send_response({"jsonrpc": "2.0", "id": None, "error": {"code": -32600, "message": "Invalid Request"}})
207
+ continue
208
+ try:
209
+ req = json.loads(line)
210
+ except Exception:
211
+ send_response({"jsonrpc": "2.0", "id": None, "error": {"code": -32700, "message": "Parse error"}})
212
+ continue
213
+ # JSON-RPC requests are objects; arrays and scalar values must not
214
+ # reach req.get() and crash the stdio server.
215
+ if not isinstance(req, dict):
216
+ send_response({"jsonrpc": "2.0", "id": None, "error": {"code": -32600, "message": "Invalid Request"}})
217
+ continue
218
+ if req.get("jsonrpc") != "2.0" or not isinstance(req.get("method"), str):
219
+ send_response({"jsonrpc": "2.0", "id": req.get("id"), "error": {"code": -32600, "message": "Invalid Request"}})
220
+ continue
221
+ if "params" in req and not isinstance(req["params"], dict):
222
+ send_response({"jsonrpc": "2.0", "id": req.get("id"), "error": {"code": -32600, "message": "Invalid Request"}})
223
+ continue
224
+
225
+ msg_id = req.get("id")
226
+ method = req.get("method")
227
+ params = req.get("params", {})
228
+
229
+ if method == "initialize":
230
+ send_response({
231
+ "jsonrpc": "2.0",
232
+ "id": msg_id,
233
+ "result": {
234
+ "protocolVersion": "2024-11-05",
235
+ "capabilities": {
236
+ "tools": {}
237
+ },
238
+ "serverInfo": {
239
+ "name": "fable-engine",
240
+ "version": "1.3.0"
241
+ }
242
+ }
243
+ })
244
+
245
+ elif method == "notifications/initialized":
246
+ logger.info("Fable client handshake complete.")
247
+
248
+ elif method == "ping":
249
+ send_response({
250
+ "jsonrpc": "2.0",
251
+ "id": msg_id,
252
+ "result": {}
253
+ })
254
+
255
+ elif method == "tools/list":
256
+ send_response({
257
+ "jsonrpc": "2.0",
258
+ "id": msg_id,
259
+ "result": {
260
+ "tools": [TOOL_SCHEMA] + BROWSER_TOOL_SCHEMAS
261
+ }
262
+ })
263
+
264
+ elif method == "tools/call":
265
+ tool_name = params.get("name")
266
+ arguments = params.get("arguments", {})
267
+ if not isinstance(tool_name, str) or not isinstance(arguments, dict):
268
+ send_response({"jsonrpc": "2.0", "id": msg_id,
269
+ "error": {"code": -32600, "message": "Invalid Request"}})
270
+ continue
271
+
272
+ if tool_name == "fable_session":
273
+ try:
274
+ result_text = handle_fable_session(arguments)
275
+ send_response({
276
+ "jsonrpc": "2.0",
277
+ "id": msg_id,
278
+ "result": {
279
+ "content": [
280
+ {
281
+ "type": "text",
282
+ "text": result_text
283
+ }
284
+ ],
285
+ "isError": False
286
+ }
287
+ })
288
+ except Exception as ex:
289
+ logger.error(f"Error handling fable_session: {ex}", exc_info=True)
290
+ send_response({
291
+ "jsonrpc": "2.0",
292
+ "id": msg_id,
293
+ "result": {
294
+ "content": [
295
+ {
296
+ "type": "text",
297
+ "text": f"Fable Engine Error: {str(ex)}"
298
+ }
299
+ ],
300
+ "isError": True
301
+ }
302
+ })
303
+ elif tool_name in BROWSER_TOOL_NAMES:
304
+ try:
305
+ sid = arguments.get("session_id")
306
+ res: Any = None
307
+
308
+ if tool_name in ("browser_open", "browser_navigate"):
309
+ timeout = bound_browser_timeout(
310
+ arguments.get("timeout", DEFAULT_BROWSER_OPEN_TIMEOUT_SECONDS)
311
+ )
312
+
313
+ if tool_name == "browser_close":
314
+ res = GLOBAL_BROWSER_ENGINE.close_session(sid)
315
+ elif tool_name == "browser_wait":
316
+ sec = float(arguments.get("seconds", 1.0))
317
+ import time
318
+ bounded_sec = min(max(sec, 0), 10)
319
+ time.sleep(bounded_sec)
320
+ res = {"status": "waited", "seconds": bounded_sec}
321
+ else:
322
+ session = GLOBAL_BROWSER_ENGINE.get_or_create_session(sid)
323
+
324
+ if tool_name in ("browser_open", "browser_navigate"):
325
+ url = str(arguments.get("url", ""))
326
+ res = session.open(url, timeout=timeout)
327
+ elif tool_name == "browser_click":
328
+ elem_id = str(arguments.get("element_id", ""))
329
+ res = session.click(elem_id)
330
+ elif tool_name == "browser_type":
331
+ elem_id = str(arguments.get("element_id", ""))
332
+ txt = str(arguments.get("text", ""))
333
+ res = session.type_text(elem_id, txt)
334
+ elif tool_name == "browser_scroll":
335
+ delta_y = int(arguments.get("delta_y", 0))
336
+ res = session.scroll(delta_y)
337
+ elif tool_name == "browser_snapshot_layers":
338
+ max_l = int(arguments.get("max_layers", 3))
339
+ res = session.snapshot_layers(max_layers=max_l)
340
+ elif tool_name == "browser_screenshot":
341
+ res = session.snapshot_viewport()
342
+ elif tool_name == "browser_close":
343
+ pass
344
+ elif tool_name == "browser_back":
345
+ res = session.back()
346
+ elif tool_name == "browser_forward":
347
+ res = session.forward()
348
+ elif tool_name == "browser_reload":
349
+ res = session.reload()
350
+ elif tool_name == "browser_wait":
351
+ pass
352
+ elif tool_name == "browser_press":
353
+ key = str(arguments.get("key", ""))
354
+ elem_id = arguments.get("element_id")
355
+ res = session.press_key(key, elem_id)
356
+ else:
357
+ raise ValueError(f"Unknown browser tool action: {tool_name}")
358
+
359
+ is_error = (
360
+ isinstance(res, dict)
361
+ and (res.get("status") == "error" or "error" in res)
362
+ )
363
+ send_response({
364
+ "jsonrpc": "2.0",
365
+ "id": msg_id,
366
+ "result": {
367
+ "content": [
368
+ {
369
+ "type": "text",
370
+ "text": json.dumps(res, indent=2)
371
+ }
372
+ ],
373
+ "isError": is_error
374
+ }
375
+ })
376
+ except Exception as ex:
377
+ logger.error(f"Error handling {tool_name}: {ex}", exc_info=True)
378
+ send_response({
379
+ "jsonrpc": "2.0",
380
+ "id": msg_id,
381
+ "result": {
382
+ "content": [
383
+ {
384
+ "type": "text",
385
+ "text": f"Stealth Browser Error: {str(ex)}"
386
+ }
387
+ ],
388
+ "isError": True
389
+ }
390
+ })
391
+ else:
392
+ send_response({
393
+ "jsonrpc": "2.0",
394
+ "id": msg_id,
395
+ "error": {
396
+ "code": -32601,
397
+ "message": f"Method / Tool '{tool_name}' not found."
398
+ }
399
+ })
400
+
401
+ else:
402
+ if msg_id is not None:
403
+ send_response({
404
+ "jsonrpc": "2.0",
405
+ "id": msg_id,
406
+ "error": {
407
+ "code": -32601,
408
+ "message": f"Unrecognized JSON-RPC method: {method}"
409
+ }
410
+ })
411
+
412
+
413
+ if __name__ == "__main__":
414
+ main()