splitagent 0.0.3__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.
- splitagent/__init__.py +8 -0
- splitagent/__main__.py +6 -0
- splitagent/agents/__init__.py +10 -0
- splitagent/agents/base.py +477 -0
- splitagent/agents/blue.py +57 -0
- splitagent/agents/chat.py +60 -0
- splitagent/agents/prompts.py +462 -0
- splitagent/agents/red.py +75 -0
- splitagent/cli.py +701 -0
- splitagent/config.py +697 -0
- splitagent/core/__init__.py +19 -0
- splitagent/core/bus.py +62 -0
- splitagent/core/context.py +587 -0
- splitagent/core/context_manager.py +381 -0
- splitagent/core/engine.py +424 -0
- splitagent/core/models.py +310 -0
- splitagent/core/proc.py +73 -0
- splitagent/core/sandbox.py +184 -0
- splitagent/core/toolbox.py +520 -0
- splitagent/core/workspace.py +420 -0
- splitagent/desktop/__init__.py +7 -0
- splitagent/desktop/api.py +525 -0
- splitagent/desktop/app.py +1131 -0
- splitagent/desktop/web/app.js +3067 -0
- splitagent/desktop/web/assets/Inter.ttf +0 -0
- splitagent/desktop/web/assets/JetBrainsMonoNerdFontMono-Regular.woff2 +0 -0
- splitagent/desktop/web/index.html +760 -0
- splitagent/desktop/web/styles.css +1612 -0
- splitagent/errors.py +27 -0
- splitagent/llm/__init__.py +8 -0
- splitagent/llm/client.py +488 -0
- splitagent/llm/types.py +172 -0
- splitagent/report/__init__.py +9 -0
- splitagent/report/cvss.py +93 -0
- splitagent/report/generator.py +733 -0
- splitagent/tools/__init__.py +8 -0
- splitagent/tools/base.py +135 -0
- splitagent/tools/defense.py +475 -0
- splitagent/tools/exploit.py +318 -0
- splitagent/tools/http_pool.py +109 -0
- splitagent/tools/knowledge.py +376 -0
- splitagent/tools/recon.py +182 -0
- splitagent/tools/registry.py +62 -0
- splitagent/tools/validate.py +908 -0
- splitagent/tools/web.py +386 -0
- splitagent/tools/workspace_tools.py +411 -0
- splitagent/ui/__init__.py +5 -0
- splitagent/ui/app.py +389 -0
- splitagent/ui/stream.py +234 -0
- splitagent/ui/theme.py +72 -0
- splitagent-0.0.3.dist-info/METADATA +987 -0
- splitagent-0.0.3.dist-info/RECORD +56 -0
- splitagent-0.0.3.dist-info/WHEEL +5 -0
- splitagent-0.0.3.dist-info/entry_points.txt +2 -0
- splitagent-0.0.3.dist-info/licenses/LICENSE +21 -0
- splitagent-0.0.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
"""Python <-> JavaScript bridge for the desktop app.
|
|
2
|
+
|
|
3
|
+
Every method here is callable from the front-end as
|
|
4
|
+
``window.pywebview.api.<method>(...)`` and must return JSON-serialisable data.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import dataclasses
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from splitagent import __version__
|
|
16
|
+
from splitagent.config import (
|
|
17
|
+
PROVIDER_PRESETS,
|
|
18
|
+
LLMSettings,
|
|
19
|
+
ensure_provider,
|
|
20
|
+
global_config_path,
|
|
21
|
+
load_project_config,
|
|
22
|
+
project_config_path,
|
|
23
|
+
save_global_config,
|
|
24
|
+
save_project_config,
|
|
25
|
+
)
|
|
26
|
+
from splitagent.core.sandbox import TARGET_PRESETS
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _project_dict(project: Any) -> dict[str, Any]:
|
|
30
|
+
"""The project as JSON, with auth secrets masked before they reach the UI."""
|
|
31
|
+
data = dataclasses.asdict(project)
|
|
32
|
+
auth = getattr(project, "auth", None)
|
|
33
|
+
if auth is not None and "auth" in data:
|
|
34
|
+
data["auth"] = auth.redacted()
|
|
35
|
+
return data
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class JsApi:
|
|
39
|
+
"""Exposed to the WebView as ``window.pywebview.api``."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, app: Any) -> None:
|
|
42
|
+
self._app = app
|
|
43
|
+
|
|
44
|
+
# -- bootstrap --------------------------------------------------------- #
|
|
45
|
+
def bootstrap(self) -> dict[str, Any]:
|
|
46
|
+
config = self._app.global_config
|
|
47
|
+
project = self._app.project
|
|
48
|
+
return {
|
|
49
|
+
"version": __version__,
|
|
50
|
+
"configured": config.configured,
|
|
51
|
+
"config": {
|
|
52
|
+
"provider": config.llm.provider,
|
|
53
|
+
"protocol": config.llm.protocol,
|
|
54
|
+
"base_url": config.llm.base_url,
|
|
55
|
+
"model": config.llm.model,
|
|
56
|
+
"temperature": config.llm.temperature,
|
|
57
|
+
"max_tokens": config.llm.max_tokens,
|
|
58
|
+
"api_key_set": bool(config.llm.resolved_api_key()),
|
|
59
|
+
},
|
|
60
|
+
"config_path": str(global_config_path()),
|
|
61
|
+
"providers": PROVIDER_PRESETS,
|
|
62
|
+
"project": _project_dict(project),
|
|
63
|
+
"project_path": str(project_config_path()),
|
|
64
|
+
"target_presets": {
|
|
65
|
+
name: {"image": preset["image"], "url": preset["url"]}
|
|
66
|
+
for name, preset in TARGET_PRESETS.items()
|
|
67
|
+
},
|
|
68
|
+
"running": self._app.is_running(),
|
|
69
|
+
"chatting": self._app.is_chatting(),
|
|
70
|
+
"sessions": self._app.list_sessions(),
|
|
71
|
+
"execution": dataclasses.asdict(project.run.execution),
|
|
72
|
+
"ui": {
|
|
73
|
+
"audits_completed": config.ui.audits_completed,
|
|
74
|
+
"show_thinking": config.ui.show_thinking,
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# -- configuration ----------------------------------------------------- #
|
|
79
|
+
def save_llm_config(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
80
|
+
config = self._app.global_config
|
|
81
|
+
provider = str(payload.get("provider") or config.llm.provider)
|
|
82
|
+
preset = PROVIDER_PRESETS.get(provider, {})
|
|
83
|
+
provider_cfg = ensure_provider(config, provider)
|
|
84
|
+
|
|
85
|
+
if payload.get("base_url"):
|
|
86
|
+
provider_cfg.base_url = str(payload["base_url"]).strip()
|
|
87
|
+
elif preset.get("base_url"):
|
|
88
|
+
provider_cfg.base_url = preset["base_url"]
|
|
89
|
+
provider_cfg.protocol = preset.get("protocol", provider_cfg.protocol or "openai")
|
|
90
|
+
key = str(payload.get("api_key") or "").strip()
|
|
91
|
+
if key:
|
|
92
|
+
provider_cfg.api_key = key
|
|
93
|
+
provider_cfg.enabled = True
|
|
94
|
+
|
|
95
|
+
if payload.get("temperature") is not None:
|
|
96
|
+
try:
|
|
97
|
+
config.llm.temperature = float(payload["temperature"])
|
|
98
|
+
except (TypeError, ValueError):
|
|
99
|
+
pass
|
|
100
|
+
if payload.get("max_tokens"):
|
|
101
|
+
try:
|
|
102
|
+
config.llm.max_tokens = int(payload["max_tokens"])
|
|
103
|
+
except (TypeError, ValueError):
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
model = str(payload.get("model") or "").strip()
|
|
107
|
+
if model:
|
|
108
|
+
provider_cfg.models.setdefault(model, model)
|
|
109
|
+
self._app.activate_model(provider, model)
|
|
110
|
+
|
|
111
|
+
config.authorized = True
|
|
112
|
+
path = save_global_config(config)
|
|
113
|
+
return {"ok": True, "path": str(path), "configured": config.configured}
|
|
114
|
+
|
|
115
|
+
# -- providers & model visibility -------------------------------------- #
|
|
116
|
+
# -- interface preferences --------------------------------------------- #
|
|
117
|
+
def set_ui_preference(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
118
|
+
"""Persist a single UI preference (currently ``show_thinking``)."""
|
|
119
|
+
config = self._app.global_config
|
|
120
|
+
if "show_thinking" in (payload or {}):
|
|
121
|
+
config.ui.show_thinking = bool(payload["show_thinking"])
|
|
122
|
+
save_global_config(config)
|
|
123
|
+
return {"ok": True, "ui": dataclasses.asdict(config.ui)}
|
|
124
|
+
|
|
125
|
+
# -- toolbox ----------------------------------------------------------- #
|
|
126
|
+
def toolbox_status(self) -> dict[str, Any]:
|
|
127
|
+
return self._app.toolbox_status()
|
|
128
|
+
|
|
129
|
+
def toolbox_setup(self, payload: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
130
|
+
payload = payload or {}
|
|
131
|
+
return self._app.toolbox_setup(
|
|
132
|
+
edition=str(payload.get("edition") or ""),
|
|
133
|
+
build=payload.get("build", True),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def toolbox_action(self, action: str, edition: str = "") -> dict[str, Any]:
|
|
137
|
+
return self._app.toolbox_action(action, edition)
|
|
138
|
+
|
|
139
|
+
def toolbox_install(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
140
|
+
return self._app.toolbox_install(
|
|
141
|
+
str(payload.get("manager") or "apt"), str(payload.get("package") or "")
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# -- workspace --------------------------------------------------------- #
|
|
145
|
+
def workspace_info(self) -> dict[str, Any]:
|
|
146
|
+
from splitagent.core.workspace import available_installers, workspace_for
|
|
147
|
+
|
|
148
|
+
workspace = workspace_for(self._app.project, base=Path.cwd())
|
|
149
|
+
workspace.ensure()
|
|
150
|
+
return {
|
|
151
|
+
"ok": True,
|
|
152
|
+
"root": str(workspace.root),
|
|
153
|
+
"installers": available_installers(),
|
|
154
|
+
"allow_install": self._app.project.workspace.allow_install,
|
|
155
|
+
"allow_external_tools": self._app.project.workspace.allow_external_tools,
|
|
156
|
+
"stats": workspace.stats(),
|
|
157
|
+
"inventory": workspace.inventory()[:80],
|
|
158
|
+
"instructions": workspace.instructions(self._app.project)[:8000],
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
def workspace_open(self) -> dict[str, Any]:
|
|
162
|
+
info = self.workspace_info()
|
|
163
|
+
return self.open_path(info["root"])
|
|
164
|
+
|
|
165
|
+
def list_providers(self) -> dict[str, Any]:
|
|
166
|
+
return self._app.list_providers()
|
|
167
|
+
|
|
168
|
+
def connect_provider(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
169
|
+
return self._app.connect_provider(payload or {})
|
|
170
|
+
|
|
171
|
+
def disconnect_provider(self, provider_id: str) -> dict[str, Any]:
|
|
172
|
+
return self._app.disconnect_provider(provider_id)
|
|
173
|
+
|
|
174
|
+
def refresh_provider(self, provider_id: str) -> dict[str, Any]:
|
|
175
|
+
return self._app.refresh_provider(provider_id)
|
|
176
|
+
|
|
177
|
+
def activate_model(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
178
|
+
return self._app.activate_model(
|
|
179
|
+
str(payload.get("provider") or ""), str(payload.get("model") or "")
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
def set_model_visibility(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
183
|
+
return self._app.set_model_visibility(
|
|
184
|
+
str(payload.get("provider") or ""),
|
|
185
|
+
str(payload.get("model") or ""),
|
|
186
|
+
bool(payload.get("visible")),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def set_provider_visibility(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
190
|
+
return self._app.set_provider_visibility(
|
|
191
|
+
str(payload.get("provider") or ""), bool(payload.get("visible"))
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def add_custom_model(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
195
|
+
return self._app.add_custom_model(
|
|
196
|
+
str(payload.get("provider") or ""),
|
|
197
|
+
str(payload.get("model") or ""),
|
|
198
|
+
str(payload.get("name") or ""),
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def remove_model(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
202
|
+
return self._app.remove_model(
|
|
203
|
+
str(payload.get("provider") or ""), str(payload.get("model") or "")
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def test_connection(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
207
|
+
from splitagent.llm.client import LLMClient
|
|
208
|
+
|
|
209
|
+
settings = LLMSettings(**vars(self._app.global_config.llm))
|
|
210
|
+
if payload:
|
|
211
|
+
settings.provider = str(payload.get("provider") or settings.provider)
|
|
212
|
+
settings.protocol = PROVIDER_PRESETS.get(settings.provider, {}).get(
|
|
213
|
+
"protocol", settings.protocol
|
|
214
|
+
)
|
|
215
|
+
if payload.get("base_url"):
|
|
216
|
+
settings.base_url = str(payload["base_url"]).strip()
|
|
217
|
+
if payload.get("model"):
|
|
218
|
+
settings.model = str(payload["model"]).strip()
|
|
219
|
+
if payload.get("api_key"):
|
|
220
|
+
settings.api_key = str(payload["api_key"]).strip()
|
|
221
|
+
|
|
222
|
+
async def _probe() -> tuple[bool, str]:
|
|
223
|
+
async with LLMClient(settings) as client:
|
|
224
|
+
return await client.test_connection()
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
ok, message = asyncio.run(_probe())
|
|
228
|
+
except Exception as exc:
|
|
229
|
+
return {"ok": False, "message": f"{type(exc).__name__}: {exc}"}
|
|
230
|
+
return {"ok": ok, "message": message}
|
|
231
|
+
|
|
232
|
+
# -- project ----------------------------------------------------------- #
|
|
233
|
+
def save_project(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
234
|
+
project = self._app.project
|
|
235
|
+
target = payload.get("target") or {}
|
|
236
|
+
run = payload.get("run") or {}
|
|
237
|
+
if target.get("kind"):
|
|
238
|
+
project.target.kind = str(target["kind"])
|
|
239
|
+
if "url" in target:
|
|
240
|
+
project.target.url = str(target.get("url") or "")
|
|
241
|
+
if "hosts" in target:
|
|
242
|
+
project.target.hosts = _as_list(target["hosts"])
|
|
243
|
+
if "ports" in target:
|
|
244
|
+
project.target.ports = _as_int_list(target["ports"])
|
|
245
|
+
if "scope" in target:
|
|
246
|
+
project.target.scope = _as_list(target["scope"])
|
|
247
|
+
if "out_of_scope" in target:
|
|
248
|
+
project.target.out_of_scope = _as_list(target["out_of_scope"])
|
|
249
|
+
if "hosts" in target:
|
|
250
|
+
project.target.hosts = _as_list(target["hosts"])
|
|
251
|
+
if run.get("rounds") is not None:
|
|
252
|
+
try:
|
|
253
|
+
project.run.rounds = max(1, int(run["rounds"]))
|
|
254
|
+
except (TypeError, ValueError):
|
|
255
|
+
pass
|
|
256
|
+
if run.get("max_steps") is not None:
|
|
257
|
+
try:
|
|
258
|
+
project.run.max_steps = max(1, int(run["max_steps"]))
|
|
259
|
+
except (TypeError, ValueError):
|
|
260
|
+
pass
|
|
261
|
+
if run.get("safe_mode") is not None:
|
|
262
|
+
project.run.safe_mode = bool(run["safe_mode"])
|
|
263
|
+
if run.get("allow_network") is not None:
|
|
264
|
+
project.run.allow_network = bool(run["allow_network"])
|
|
265
|
+
if run.get("tool_concurrency") is not None:
|
|
266
|
+
try:
|
|
267
|
+
project.run.tool_concurrency = max(1, int(run["tool_concurrency"]))
|
|
268
|
+
except (TypeError, ValueError):
|
|
269
|
+
pass
|
|
270
|
+
if run.get("max_duration_minutes") is not None:
|
|
271
|
+
try:
|
|
272
|
+
project.run.max_duration_minutes = max(0, int(run["max_duration_minutes"]))
|
|
273
|
+
except (TypeError, ValueError):
|
|
274
|
+
pass
|
|
275
|
+
sandbox = run.get("sandbox") or {}
|
|
276
|
+
if sandbox:
|
|
277
|
+
if "enabled" in sandbox:
|
|
278
|
+
project.run.sandbox.enabled = bool(sandbox["enabled"])
|
|
279
|
+
if sandbox.get("image"):
|
|
280
|
+
project.run.sandbox.image = str(sandbox["image"])
|
|
281
|
+
if sandbox.get("port_map"):
|
|
282
|
+
port_map: dict[str, int] = {}
|
|
283
|
+
for key, value in sandbox["port_map"].items():
|
|
284
|
+
try:
|
|
285
|
+
port_map[str(key)] = int(value)
|
|
286
|
+
except (TypeError, ValueError):
|
|
287
|
+
continue # ignore a non-numeric port instead of crashing
|
|
288
|
+
project.run.sandbox.port_map = port_map
|
|
289
|
+
workspace = payload.get("workspace") or {}
|
|
290
|
+
if workspace:
|
|
291
|
+
if "path" in workspace:
|
|
292
|
+
project.workspace.path = str(workspace.get("path") or "")
|
|
293
|
+
for key in ("allow_install", "allow_external_tools"):
|
|
294
|
+
if workspace.get(key) is not None:
|
|
295
|
+
setattr(project.workspace, key, bool(workspace[key]))
|
|
296
|
+
if workspace.get("max_install_seconds") is not None:
|
|
297
|
+
try:
|
|
298
|
+
project.workspace.max_install_seconds = max(
|
|
299
|
+
30, int(workspace["max_install_seconds"])
|
|
300
|
+
)
|
|
301
|
+
except (TypeError, ValueError):
|
|
302
|
+
pass
|
|
303
|
+
if "instructions" in workspace:
|
|
304
|
+
project.workspace.instructions = str(workspace.get("instructions") or "")
|
|
305
|
+
if "instruction_files" in workspace:
|
|
306
|
+
project.workspace.instruction_files = _as_list(workspace["instruction_files"])
|
|
307
|
+
auth = payload.get("auth") or {}
|
|
308
|
+
if auth:
|
|
309
|
+
if "username" in auth:
|
|
310
|
+
project.auth.username = str(auth.get("username") or "")
|
|
311
|
+
if "password" in auth:
|
|
312
|
+
project.auth.password = str(auth.get("password") or "")
|
|
313
|
+
if "token" in auth:
|
|
314
|
+
project.auth.token = str(auth.get("token") or "")
|
|
315
|
+
if "cookies" in auth:
|
|
316
|
+
project.auth.cookies = str(auth.get("cookies") or "")
|
|
317
|
+
if isinstance(auth.get("headers"), dict):
|
|
318
|
+
project.auth.headers = {str(k): str(v) for k, v in auth["headers"].items() if k}
|
|
319
|
+
if payload.get("name"):
|
|
320
|
+
project.name = str(payload["name"])
|
|
321
|
+
|
|
322
|
+
path = project_config_path()
|
|
323
|
+
if payload.get("path"):
|
|
324
|
+
path = Path(str(payload["path"]))
|
|
325
|
+
save_project_config(project, path)
|
|
326
|
+
return {"ok": True, "path": str(path), "project": _project_dict(project)}
|
|
327
|
+
|
|
328
|
+
def load_project(self, path: str = "") -> dict[str, Any]:
|
|
329
|
+
candidate = Path(path) if path else project_config_path()
|
|
330
|
+
if not candidate.exists():
|
|
331
|
+
return {"ok": False, "error": f"not found: {candidate}"}
|
|
332
|
+
project = load_project_config(candidate)
|
|
333
|
+
self._app.project = project
|
|
334
|
+
return {"ok": True, "path": str(candidate), "project": _project_dict(project)}
|
|
335
|
+
|
|
336
|
+
def choose_project_file(self) -> dict[str, Any]:
|
|
337
|
+
import webview
|
|
338
|
+
|
|
339
|
+
result = self._app.window.create_file_dialog(
|
|
340
|
+
webview.FileDialog.OPEN,
|
|
341
|
+
allow_multiple=False,
|
|
342
|
+
file_types=("SplitAgent config (*.yaml;*.yml)", "All files (*.*)"),
|
|
343
|
+
)
|
|
344
|
+
if not result:
|
|
345
|
+
return {"ok": False}
|
|
346
|
+
return self.load_project(str(result[0]))
|
|
347
|
+
|
|
348
|
+
def pick_scope_document(self) -> dict[str, Any]:
|
|
349
|
+
"""Let the operator pick a scope/objectives document for the planner.
|
|
350
|
+
|
|
351
|
+
Reads text formats only (md, txt, json, yaml, csv). The content is
|
|
352
|
+
returned to the UI, which stores it in the brief; it never executes.
|
|
353
|
+
"""
|
|
354
|
+
import webview
|
|
355
|
+
|
|
356
|
+
if self._app.window is None:
|
|
357
|
+
return {"ok": False, "error": "window not ready"}
|
|
358
|
+
result = self._app.window.create_file_dialog(
|
|
359
|
+
webview.FileDialog.OPEN,
|
|
360
|
+
allow_multiple=False,
|
|
361
|
+
file_types=(
|
|
362
|
+
"Documents (*.md;*.txt;*.json;*.yaml;*.yml;*.csv)",
|
|
363
|
+
"All files (*.*)",
|
|
364
|
+
),
|
|
365
|
+
)
|
|
366
|
+
if not result:
|
|
367
|
+
return {"ok": False}
|
|
368
|
+
path = Path(str(result[0]))
|
|
369
|
+
if not path.is_file():
|
|
370
|
+
return {"ok": False, "error": "not a file"}
|
|
371
|
+
try:
|
|
372
|
+
content = path.read_text(encoding="utf-8", errors="replace")
|
|
373
|
+
except OSError as exc:
|
|
374
|
+
return {"ok": False, "error": f"could not read: {exc}"}
|
|
375
|
+
return {
|
|
376
|
+
"ok": True,
|
|
377
|
+
"name": path.name,
|
|
378
|
+
"path": str(path),
|
|
379
|
+
"content": content[:20000],
|
|
380
|
+
"truncated": len(content) > 20000,
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
def apply_target_preset(self, name: str) -> dict[str, Any]:
|
|
384
|
+
preset = TARGET_PRESETS.get(name)
|
|
385
|
+
if not preset:
|
|
386
|
+
return {"ok": False, "error": f"unknown preset '{name}'"}
|
|
387
|
+
project = self._app.project
|
|
388
|
+
project.target.url = preset["url"]
|
|
389
|
+
project.target.ports = [int(p) for p in preset["port_map"]]
|
|
390
|
+
project.run.sandbox.image = preset["image"]
|
|
391
|
+
project.run.sandbox.port_map = preset["port_map"]
|
|
392
|
+
return {"ok": True, "project": _project_dict(project)}
|
|
393
|
+
|
|
394
|
+
# -- audit ------------------------------------------------------------- #
|
|
395
|
+
def start_audit(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
396
|
+
return self._app.start_audit(payload or {})
|
|
397
|
+
|
|
398
|
+
def stop_audit(self) -> dict[str, Any]:
|
|
399
|
+
return self._app.stop_audit()
|
|
400
|
+
|
|
401
|
+
# -- copilot ----------------------------------------------------------- #
|
|
402
|
+
def chat_send(self, message: str) -> dict[str, Any]:
|
|
403
|
+
return self._app.chat_send(message)
|
|
404
|
+
|
|
405
|
+
def chat_reset(self) -> dict[str, Any]:
|
|
406
|
+
return self._app.chat_reset()
|
|
407
|
+
|
|
408
|
+
def chat_stop(self) -> dict[str, Any]:
|
|
409
|
+
return self._app.chat_stop()
|
|
410
|
+
|
|
411
|
+
def chat_state(self, since: int = 0) -> dict[str, Any]:
|
|
412
|
+
return self._app.chat_state(since)
|
|
413
|
+
|
|
414
|
+
# -- engagement planner ------------------------------------------------- #
|
|
415
|
+
def build_audit_plan(self, brief: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
416
|
+
return self._app.build_audit_plan(brief or {})
|
|
417
|
+
|
|
418
|
+
def brief_chat(self, payload: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
419
|
+
return self._app.brief_chat(payload or {})
|
|
420
|
+
|
|
421
|
+
def list_models(self) -> dict[str, Any]:
|
|
422
|
+
return self._app.list_models()
|
|
423
|
+
|
|
424
|
+
def list_sessions(self) -> list[dict[str, Any]]:
|
|
425
|
+
return self._app.list_sessions()
|
|
426
|
+
|
|
427
|
+
def load_session(self, session_id: str) -> dict[str, Any]:
|
|
428
|
+
return self._app.load_session(session_id)
|
|
429
|
+
|
|
430
|
+
def export_report(self, formats: list[str] | None = None) -> dict[str, Any]:
|
|
431
|
+
return self._app.export_report(formats)
|
|
432
|
+
|
|
433
|
+
def save_trace(self, payload: str) -> dict[str, Any]:
|
|
434
|
+
"""Persist the full client-side replay trace next to the sessions."""
|
|
435
|
+
from datetime import datetime
|
|
436
|
+
|
|
437
|
+
from splitagent.config import config_home
|
|
438
|
+
|
|
439
|
+
try:
|
|
440
|
+
directory = config_home() / "traces"
|
|
441
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
442
|
+
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
443
|
+
path = directory / f"trace-{stamp}.json"
|
|
444
|
+
path.write_text(str(payload), encoding="utf-8")
|
|
445
|
+
except Exception as exc:
|
|
446
|
+
return {"ok": False, "error": str(exc)}
|
|
447
|
+
return {"ok": True, "path": str(path)}
|
|
448
|
+
|
|
449
|
+
# -- diagnostics ------------------------------------------------------- #
|
|
450
|
+
def log_js_error(self, message: str) -> dict[str, Any]:
|
|
451
|
+
from datetime import datetime
|
|
452
|
+
|
|
453
|
+
from splitagent.config import config_home
|
|
454
|
+
|
|
455
|
+
try:
|
|
456
|
+
home = config_home()
|
|
457
|
+
home.mkdir(parents=True, exist_ok=True)
|
|
458
|
+
with (home / "desktop-js.log").open("a", encoding="utf-8") as handle:
|
|
459
|
+
handle.write(f"{datetime.now().isoformat(timespec='seconds')} {message}\n")
|
|
460
|
+
except Exception: # pragma: no cover - best effort
|
|
461
|
+
pass
|
|
462
|
+
return {"ok": True}
|
|
463
|
+
|
|
464
|
+
# -- shell ------------------------------------------------------------- #
|
|
465
|
+
def window_action(self, action: str) -> dict[str, Any]:
|
|
466
|
+
window = self._app.window
|
|
467
|
+
try:
|
|
468
|
+
if action == "minimize":
|
|
469
|
+
window.minimize()
|
|
470
|
+
elif action == "maximize":
|
|
471
|
+
if getattr(self._app, "_maximized", False):
|
|
472
|
+
window.restore()
|
|
473
|
+
self._app._maximized = False
|
|
474
|
+
else:
|
|
475
|
+
window.maximize()
|
|
476
|
+
self._app._maximized = True
|
|
477
|
+
elif action == "close":
|
|
478
|
+
window.destroy()
|
|
479
|
+
elif action == "fullscreen":
|
|
480
|
+
window.toggle_fullscreen()
|
|
481
|
+
except Exception as exc:
|
|
482
|
+
return {"ok": False, "error": str(exc)}
|
|
483
|
+
return {"ok": True}
|
|
484
|
+
|
|
485
|
+
def open_path(self, path: str) -> dict[str, Any]:
|
|
486
|
+
target = Path(path)
|
|
487
|
+
if not target.exists():
|
|
488
|
+
return {"ok": False, "error": "path not found"}
|
|
489
|
+
try:
|
|
490
|
+
os.startfile(str(target)) # type: ignore[attr-defined]
|
|
491
|
+
except Exception as exc:
|
|
492
|
+
return {"ok": False, "error": str(exc)}
|
|
493
|
+
return {"ok": True}
|
|
494
|
+
|
|
495
|
+
def reveal(self, path: str) -> dict[str, Any]:
|
|
496
|
+
target = Path(path)
|
|
497
|
+
try:
|
|
498
|
+
os.startfile(str(target.parent if target.is_file() else target)) # type: ignore[attr-defined]
|
|
499
|
+
except Exception as exc:
|
|
500
|
+
return {"ok": False, "error": str(exc)}
|
|
501
|
+
return {"ok": True}
|
|
502
|
+
|
|
503
|
+
def get_session_state(self) -> dict[str, Any]:
|
|
504
|
+
context = self._app.session
|
|
505
|
+
if context is None:
|
|
506
|
+
return {"ok": False}
|
|
507
|
+
return {"ok": True, "state": context.state.to_dict()}
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _as_list(value: Any) -> list[str]:
|
|
511
|
+
if isinstance(value, list):
|
|
512
|
+
return [str(v).strip() for v in value if str(v).strip()]
|
|
513
|
+
if isinstance(value, str):
|
|
514
|
+
return [part.strip() for part in value.split(",") if part.strip()]
|
|
515
|
+
return []
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _as_int_list(value: Any) -> list[int]:
|
|
519
|
+
result: list[int] = []
|
|
520
|
+
for item in _as_list(value):
|
|
521
|
+
try:
|
|
522
|
+
result.append(int(item))
|
|
523
|
+
except ValueError:
|
|
524
|
+
continue
|
|
525
|
+
return result
|