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.
- fable_compressor.py +356 -0
- fable_engine/__init__.py +1 -0
- fable_engine/actions/__init__.py +291 -0
- fable_engine/actions/cas.py +182 -0
- fable_engine/actions/deliberation.py +523 -0
- fable_engine/actions/fleet.py +807 -0
- fable_engine/actions/lifecycle.py +298 -0
- fable_engine/actions/scrapers.py +116 -0
- fable_engine/actions/system3.py +815 -0
- fable_engine/browser.py +824 -0
- fable_engine/cas.py +974 -0
- fable_engine/fable_session.json +510 -0
- fable_engine/guards.py +283 -0
- fable_engine/schema.py +714 -0
- fable_engine/scrapers/__init__.py +32 -0
- fable_engine/scrapers/arxiv.py +115 -0
- fable_engine/scrapers/base.py +386 -0
- fable_engine/scrapers/github.py +129 -0
- fable_engine/scrapers/reddit.py +154 -0
- fable_engine/scrapers/web.py +120 -0
- fable_engine/scrapers/x.py +125 -0
- fable_engine/scrapers/youtube.py +132 -0
- fable_engine/server.py +414 -0
- fable_engine/session.py +1819 -0
- fable_engine/test_server.py +1362 -0
- fable_engine/updater.py +541 -0
- fable_engine-1.3.1.dist-info/LICENSE +22 -0
- fable_engine-1.3.1.dist-info/METADATA +173 -0
- fable_engine-1.3.1.dist-info/RECORD +104 -0
- fable_engine-1.3.1.dist-info/WHEEL +5 -0
- fable_engine-1.3.1.dist-info/entry_points.txt +5 -0
- fable_engine-1.3.1.dist-info/top_level.txt +6 -0
- fable_mode/__init__.py +3 -0
- fable_mode/__main__.py +4 -0
- fable_mode/adapters.py +1014 -0
- fable_mode/installer.py +553 -0
- fable_mode/launcher.py +437 -0
- fable_mode/manifest.py +142 -0
- fable_mode/resources.json +114 -0
- fable_mode/safety.py +103 -0
- fable_mode_entry.py +10 -0
- fable_v2/__init__.py +146 -0
- fable_v2/adapters.py +151 -0
- fable_v2/coder_fleet/__init__.py +100 -0
- fable_v2/coder_fleet/ast_tools.py +158 -0
- fable_v2/coder_fleet/compute.py +199 -0
- fable_v2/coder_fleet/design_engine.py +1316 -0
- fable_v2/coder_fleet/diagnostics.py +293 -0
- fable_v2/coder_fleet/fleet_dispatcher.py +214 -0
- fable_v2/coder_fleet/mock_auditor.py +306 -0
- fable_v2/coder_fleet/mutation.py +216 -0
- fable_v2/coder_fleet/property_oracle.py +260 -0
- fable_v2/coder_fleet/receipt_attestor.py +122 -0
- fable_v2/coder_fleet/red_team_swarm.py +908 -0
- fable_v2/coder_fleet/test_harness.py +198 -0
- fable_v2/coder_fleet/vector_engine.py +1287 -0
- fable_v2/coder_fleet/visual.py +357 -0
- fable_v2/coder_fleet/workspace.py +153 -0
- fable_v2/cortical/__init__.py +20 -0
- fable_v2/cortical/plasticity_engine.py +992 -0
- fable_v2/execution_broker.py +811 -0
- fable_v2/proof_engine.py +1141 -0
- fable_v2/protocol.py +485 -0
- fable_v2/runtime.py +1010 -0
- fable_v2/system3/__init__.py +204 -0
- fable_v2/system3/causal.py +558 -0
- fable_v2/system3/dialectical.py +577 -0
- fable_v2/system3/evolution.py +503 -0
- fable_v2/system3/executive.py +338 -0
- fable_v2/system3/free_energy.py +479 -0
- fable_v2/system3/hyperbolic.py +555 -0
- fable_v2/system3/induction.py +336 -0
- fable_v2/system3/kripke.py +548 -0
- fable_v2/system3/oracle.py +745 -0
- fable_v2/verifiers.py +72 -0
- tests/__init__.py +1 -0
- tests/test_anti_loop_circuit_breaker.py +64 -0
- tests/test_auto_updater.py +407 -0
- tests/test_coder_fleet.py +535 -0
- tests/test_delegation_compiler.py +54 -0
- tests/test_descriptor_boundaries.py +126 -0
- tests/test_design_engine.py +603 -0
- tests/test_epistemic_evidence_validator.py +66 -0
- tests/test_execution_broker.py +233 -0
- tests/test_fable_v2.py +406 -0
- tests/test_fleet_transitions.py +116 -0
- tests/test_fsm_redteam_evolution.py +406 -0
- tests/test_goal_rubric_and_pipeline.py +367 -0
- tests/test_hebbian_plasticity.py +585 -0
- tests/test_packaging_runtime.py +194 -0
- tests/test_proof_engine.py +259 -0
- tests/test_red_team_swarm.py +645 -0
- tests/test_redteam_remediation.py +169 -0
- tests/test_registration_transaction.py +375 -0
- tests/test_requested_regressions.py +467 -0
- tests/test_scrapers.py +370 -0
- tests/test_server_actions.py +93 -0
- tests/test_server_frontier_actions.py +269 -0
- tests/test_server_protocol.py +88 -0
- tests/test_stealth_browser.py +970 -0
- tests/test_system3.py +381 -0
- tests/test_system3_deep_integration.py +385 -0
- tests/test_system3_frontier.py +436 -0
- 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()
|