webpilot-engine 2.0.6__tar.gz → 2.0.7__tar.gz
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.
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/PKG-INFO +1 -1
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/cli.py +18 -5
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/pyproject.toml +1 -1
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/supervisor/client.py +7 -2
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/supervisor/contracts.py +1 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/supervisor/master_daemon.py +34 -40
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/supervisor/shell.py +3 -3
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/supervisor/worker_process.py +16 -3
- webpilot_engine-2.0.7/tests/test_supervisor.py +179 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/tests/test_supervisor_security.py +19 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/webpilot_engine.egg-info/PKG-INFO +1 -1
- webpilot_engine-2.0.6/tests/test_supervisor.py +0 -83
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/README.md +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/adapters/__init__.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/adapters/base.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/adapters/browser_adapter.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/adapters/dom_scripts.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/adapters/envelope_builder.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/adapters/live_session_manager.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/adapters/upload_adapter.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/builder.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/common/__init__.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/common/cookies.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/common/logging.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/common/process_manager.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/common/retry.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/common/secrets.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/common/text.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/config/__init__.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/config/device_profile.example.json +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/config/device_profile.json +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/config/settings.example.json +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/config/settings.json +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/config/settings.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/constants/__init__.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/constants/contracts.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/constants/selectors.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/constants/timeouts.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/core/__init__.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/core/exceptions.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/core/models.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/engine.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/exceptions.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/mcp_server.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/models.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/orchestration/__init__.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/orchestration/context.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/orchestration/flow_orchestrator.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/orchestration/session_coordinator.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/services/__init__.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/services/auth_navigation_service.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/services/field_interaction_service.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/services/form_filler_service.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/services/inspection_formatter_service.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/services/inspection_service.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/services/reactive_interaction_service.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/setup.cfg +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/supervisor/__init__.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/supervisor/security.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/tests/test_config_and_builder.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/tests/test_contracts.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/tests/test_engine_facade.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/tests/test_enterprise_integration.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/tests/test_live_session.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/tests/test_mcp_server.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/tests/test_orchestration.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/tests/test_process_manager.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/tests/test_redaction.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/tests/test_secrets_resolution.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/tests/test_services.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/validators/__init__.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/validators/form_validator.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/validators/payload_validator.py +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/webpilot_engine.egg-info/SOURCES.txt +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/webpilot_engine.egg-info/dependency_links.txt +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/webpilot_engine.egg-info/entry_points.txt +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/webpilot_engine.egg-info/requires.txt +0 -0
- {webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/webpilot_engine.egg-info/top_level.txt +0 -0
|
@@ -40,6 +40,11 @@ def get_base_url(args: argparse.Namespace) -> str:
|
|
|
40
40
|
return f"http://{host}:{port}"
|
|
41
41
|
|
|
42
42
|
|
|
43
|
+
def get_supervisor_client(args: argparse.Namespace) -> SupervisorClient:
|
|
44
|
+
token = getattr(args, "token", None)
|
|
45
|
+
return SupervisorClient(base_url=get_base_url(args), token=token)
|
|
46
|
+
|
|
47
|
+
|
|
43
48
|
def inspect_cmd(args: argparse.Namespace) -> None:
|
|
44
49
|
"""Executes the universal inspection workflow."""
|
|
45
50
|
if getattr(args, "standalone", False):
|
|
@@ -63,7 +68,7 @@ def inspect_cmd(args: argparse.Namespace) -> None:
|
|
|
63
68
|
return
|
|
64
69
|
|
|
65
70
|
# Multi-tier Master Supervisor execution (default)
|
|
66
|
-
client =
|
|
71
|
+
client = get_supervisor_client(args)
|
|
67
72
|
|
|
68
73
|
if args.close_session:
|
|
69
74
|
res = client.stop_service()
|
|
@@ -159,7 +164,7 @@ def apply_cmd(args: argparse.Namespace) -> None:
|
|
|
159
164
|
return
|
|
160
165
|
|
|
161
166
|
# Multi-tier Master Supervisor execution (default)
|
|
162
|
-
client =
|
|
167
|
+
client = get_supervisor_client(args)
|
|
163
168
|
|
|
164
169
|
if args.close_session:
|
|
165
170
|
res = client.stop_service()
|
|
@@ -211,7 +216,7 @@ def apply_cmd(args: argparse.Namespace) -> None:
|
|
|
211
216
|
|
|
212
217
|
def service_cmd(args: argparse.Namespace) -> None:
|
|
213
218
|
"""Manages the Master Supervisor daemon (status, restart, stop, start)."""
|
|
214
|
-
client =
|
|
219
|
+
client = get_supervisor_client(args)
|
|
215
220
|
action = args.service_action.lower()
|
|
216
221
|
|
|
217
222
|
if action == "status":
|
|
@@ -259,7 +264,7 @@ def service_cmd(args: argparse.Namespace) -> None:
|
|
|
259
264
|
def shell_cmd(args: argparse.Namespace) -> None:
|
|
260
265
|
"""Launches the interactive REPL shell."""
|
|
261
266
|
from supervisor.shell import run_interactive_shell
|
|
262
|
-
run_interactive_shell(host=args.host, port=args.port)
|
|
267
|
+
run_interactive_shell(host=args.host, port=args.port, token=getattr(args, "token", None))
|
|
263
268
|
|
|
264
269
|
|
|
265
270
|
def main():
|
|
@@ -267,9 +272,12 @@ def main():
|
|
|
267
272
|
from supervisor.master_daemon import run_master_daemon
|
|
268
273
|
idx = sys.argv.index("--run-daemon")
|
|
269
274
|
port = 9333
|
|
275
|
+
token = None
|
|
270
276
|
if len(sys.argv) > idx + 1 and sys.argv[idx + 1].isdigit():
|
|
271
277
|
port = int(sys.argv[idx + 1])
|
|
272
|
-
|
|
278
|
+
if len(sys.argv) > idx + 2:
|
|
279
|
+
token = sys.argv[idx + 2]
|
|
280
|
+
run_master_daemon(port=port, token=token)
|
|
273
281
|
return
|
|
274
282
|
|
|
275
283
|
if "--run-worker" in sys.argv:
|
|
@@ -286,6 +294,7 @@ def main():
|
|
|
286
294
|
parser.add_argument("--env-file", help="Path to custom .env file")
|
|
287
295
|
parser.add_argument("--host", default="127.0.0.1", help="Supervisor host address (default: 127.0.0.1)")
|
|
288
296
|
parser.add_argument("--port", type=int, default=9333, help="Supervisor port (default: 9333)")
|
|
297
|
+
parser.add_argument("--token", help="Explicit Master Supervisor authentication token")
|
|
289
298
|
|
|
290
299
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
291
300
|
|
|
@@ -305,6 +314,7 @@ def main():
|
|
|
305
314
|
p_inspect.add_argument("--standalone", action="store_true", help="Run in-process via FlowOrchestrator instead of Master Supervisor")
|
|
306
315
|
p_inspect.add_argument("--host", default="127.0.0.1", help="Supervisor host address")
|
|
307
316
|
p_inspect.add_argument("--port", type=int, default=9333, help="Supervisor port")
|
|
317
|
+
p_inspect.add_argument("--token", help="Explicit Master Supervisor authentication token")
|
|
308
318
|
p_inspect.add_argument("--config", help="Path to custom settings.json configuration file")
|
|
309
319
|
p_inspect.add_argument("--device-profile", help="Path to custom device_profile.json configuration file")
|
|
310
320
|
p_inspect.add_argument("--env-file", help="Path to custom .env file")
|
|
@@ -329,6 +339,7 @@ def main():
|
|
|
329
339
|
p_apply.add_argument("--standalone", action="store_true", help="Run in-process via FlowOrchestrator instead of Master Supervisor")
|
|
330
340
|
p_apply.add_argument("--host", default="127.0.0.1", help="Supervisor host address")
|
|
331
341
|
p_apply.add_argument("--port", type=int, default=9333, help="Supervisor port")
|
|
342
|
+
p_apply.add_argument("--token", help="Explicit Master Supervisor authentication token")
|
|
332
343
|
p_apply.add_argument("--config", help="Path to custom settings.json configuration file")
|
|
333
344
|
p_apply.add_argument("--device-profile", help="Path to custom device_profile.json configuration file")
|
|
334
345
|
p_apply.add_argument("--env-file", help="Path to custom .env file")
|
|
@@ -337,12 +348,14 @@ def main():
|
|
|
337
348
|
p_shell = subparsers.add_parser("shell", help="Launch interactive REPL automation shell")
|
|
338
349
|
p_shell.add_argument("--host", default="127.0.0.1", help="Supervisor host address")
|
|
339
350
|
p_shell.add_argument("--port", type=int, default=9333, help="Supervisor port")
|
|
351
|
+
p_shell.add_argument("--token", help="Explicit Master Supervisor authentication token")
|
|
340
352
|
|
|
341
353
|
# 4. Supervisor Service Management Subcommand
|
|
342
354
|
p_service = subparsers.add_parser("service", help="Manage Master Supervisor background daemon")
|
|
343
355
|
p_service.add_argument("service_action", choices=["status", "restart", "stop", "start"], help="Action to execute")
|
|
344
356
|
p_service.add_argument("--host", default="127.0.0.1", help="Supervisor host address")
|
|
345
357
|
p_service.add_argument("--port", type=int, default=9333, help="Supervisor port")
|
|
358
|
+
p_service.add_argument("--token", help="Explicit Master Supervisor authentication token")
|
|
346
359
|
|
|
347
360
|
# 5. Browser Binaries Installation Subcommand
|
|
348
361
|
p_install = subparsers.add_parser("install", help="Download and configure required Playwright Chromium browser")
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "webpilot-engine"
|
|
7
|
-
version = "2.0.
|
|
7
|
+
version = "2.0.7"
|
|
8
8
|
description = "WebPilot: Enterprise Autonomous Browser Engine, Universal Form Inspector & AI Agent Web Runtime"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [
|
|
@@ -86,12 +86,17 @@ class SupervisorClient:
|
|
|
86
86
|
cwd = PROJECT_ROOT
|
|
87
87
|
cmd = [py_exe, script_path]
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
spawn_env = os.environ.copy()
|
|
90
|
+
if self.token:
|
|
91
|
+
spawn_env["WEBPILOT_SUPERVISOR_TOKEN"] = self.token
|
|
92
|
+
|
|
93
|
+
get_process_manager().launch_detached(cmd, cwd=cwd, env=spawn_env)
|
|
90
94
|
|
|
91
95
|
# Wait up to 4 seconds for supervisor to become active
|
|
92
96
|
start = time.time()
|
|
93
97
|
while time.time() - start < 4.0:
|
|
94
|
-
self.token
|
|
98
|
+
if not self.token:
|
|
99
|
+
self.token = load_supervisor_token()
|
|
95
100
|
if self.is_running():
|
|
96
101
|
print("[✓] Master Supervisor is ready and active.")
|
|
97
102
|
return
|
|
@@ -49,6 +49,7 @@ class SupervisorActionResponse:
|
|
|
49
49
|
telemetry: Dict[str, Any] = field(default_factory=dict)
|
|
50
50
|
schema: Optional[Dict[str, Any]] = None
|
|
51
51
|
confirmed_fields: List[Any] = field(default_factory=list)
|
|
52
|
+
unconfirmed_fields: List[Any] = field(default_factory=list)
|
|
52
53
|
failed_fields: List[Any] = field(default_factory=list)
|
|
53
54
|
validation_errors: List[str] = field(default_factory=list)
|
|
54
55
|
reactive_event: Optional[Dict[str, Any]] = None
|
|
@@ -88,33 +88,37 @@ class MasterSupervisor:
|
|
|
88
88
|
self.watchdog_thread = threading.Thread(target=self._watchdog_loop, daemon=True)
|
|
89
89
|
self.watchdog_thread.start()
|
|
90
90
|
|
|
91
|
+
def _spawn_worker(self) -> subprocess.Popen:
|
|
92
|
+
"""Spawns Layer 2 Worker subprocess with process group isolation."""
|
|
93
|
+
if getattr(sys, "frozen", False):
|
|
94
|
+
worker_cmd = [sys.executable, "--run-worker"]
|
|
95
|
+
worker_cwd = os.path.dirname(sys.executable)
|
|
96
|
+
else:
|
|
97
|
+
worker_cmd = [sys.executable, "-m", "supervisor.worker_process"]
|
|
98
|
+
worker_cwd = PROJECT_ROOT
|
|
99
|
+
|
|
100
|
+
worker_env = os.environ.copy()
|
|
101
|
+
worker_env["WEBPILOT_SUPERVISOR_TOKEN"] = self.token
|
|
102
|
+
|
|
103
|
+
popen_kwargs: Dict[str, Any] = {
|
|
104
|
+
"stdin": subprocess.PIPE,
|
|
105
|
+
"stdout": subprocess.PIPE,
|
|
106
|
+
"stderr": subprocess.PIPE,
|
|
107
|
+
"text": True,
|
|
108
|
+
"bufsize": 1,
|
|
109
|
+
"cwd": worker_cwd,
|
|
110
|
+
"env": worker_env,
|
|
111
|
+
}
|
|
112
|
+
if sys.platform != "win32":
|
|
113
|
+
popen_kwargs["start_new_session"] = True
|
|
114
|
+
|
|
115
|
+
return subprocess.Popen(worker_cmd, **popen_kwargs)
|
|
116
|
+
|
|
91
117
|
def _ensure_worker(self) -> subprocess.Popen:
|
|
92
118
|
"""Spawns Layer 2 Worker subprocess if not currently running."""
|
|
93
119
|
with self.lock:
|
|
94
120
|
if self.worker_process is None or self.worker_process.poll() is not None:
|
|
95
|
-
|
|
96
|
-
worker_cmd = [sys.executable, "--run-worker"]
|
|
97
|
-
worker_cwd = os.path.dirname(sys.executable)
|
|
98
|
-
else:
|
|
99
|
-
worker_cmd = [sys.executable, "-m", "supervisor.worker_process"]
|
|
100
|
-
worker_cwd = PROJECT_ROOT
|
|
101
|
-
|
|
102
|
-
worker_env = os.environ.copy()
|
|
103
|
-
worker_env["WEBPILOT_SUPERVISOR_TOKEN"] = self.token
|
|
104
|
-
|
|
105
|
-
popen_kwargs: Dict[str, Any] = {
|
|
106
|
-
"stdin": subprocess.PIPE,
|
|
107
|
-
"stdout": subprocess.PIPE,
|
|
108
|
-
"stderr": subprocess.PIPE,
|
|
109
|
-
"text": True,
|
|
110
|
-
"bufsize": 1,
|
|
111
|
-
"cwd": worker_cwd,
|
|
112
|
-
"env": worker_env,
|
|
113
|
-
}
|
|
114
|
-
if sys.platform != "win32":
|
|
115
|
-
popen_kwargs["start_new_session"] = True
|
|
116
|
-
|
|
117
|
-
self.worker_process = subprocess.Popen(worker_cmd, **popen_kwargs)
|
|
121
|
+
self.worker_process = self._spawn_worker()
|
|
118
122
|
return self.worker_process
|
|
119
123
|
|
|
120
124
|
def execute_action(self, req: SupervisorActionRequest) -> SupervisorActionResponse:
|
|
@@ -179,19 +183,8 @@ class MasterSupervisor:
|
|
|
179
183
|
with self.lock:
|
|
180
184
|
old_pid = self.worker_process.pid if self.worker_process else None
|
|
181
185
|
self.terminate_worker()
|
|
182
|
-
# Spawn fresh Worker
|
|
183
|
-
|
|
184
|
-
worker_env["WEBPILOT_SUPERVISOR_TOKEN"] = self.token
|
|
185
|
-
new_worker = subprocess.Popen(
|
|
186
|
-
[sys.executable, "-m", "supervisor.worker_process"],
|
|
187
|
-
stdin=subprocess.PIPE,
|
|
188
|
-
stdout=subprocess.PIPE,
|
|
189
|
-
stderr=subprocess.PIPE,
|
|
190
|
-
text=True,
|
|
191
|
-
bufsize=1,
|
|
192
|
-
cwd=PROJECT_ROOT,
|
|
193
|
-
env=worker_env,
|
|
194
|
-
)
|
|
186
|
+
# Spawn fresh Worker using unified helper with start_new_session on POSIX
|
|
187
|
+
new_worker = self._spawn_worker()
|
|
195
188
|
self.worker_process = new_worker
|
|
196
189
|
self.last_active_time = time.time()
|
|
197
190
|
|
|
@@ -306,13 +299,13 @@ def create_supervisor_handler(supervisor: MasterSupervisor):
|
|
|
306
299
|
return SupervisorHTTPHandler
|
|
307
300
|
|
|
308
301
|
|
|
309
|
-
def run_master_daemon(port: int = DEFAULT_SUPERVISOR_PORT):
|
|
302
|
+
def run_master_daemon(port: int = DEFAULT_SUPERVISOR_PORT, token: Optional[str] = None):
|
|
310
303
|
"""Starts the Master Supervisor HTTP service."""
|
|
311
304
|
sys.stdout = LogWriter(LOG_FILE)
|
|
312
305
|
sys.stderr = LogWriter(LOG_FILE)
|
|
313
306
|
log_daemon(f"run_master_daemon starting on port {port} (PID: {os.getpid()})")
|
|
314
307
|
try:
|
|
315
|
-
supervisor = MasterSupervisor(port=port)
|
|
308
|
+
supervisor = MasterSupervisor(port=port, token=token)
|
|
316
309
|
handler_class = create_supervisor_handler(supervisor)
|
|
317
310
|
server = HTTPServer((DEFAULT_SUPERVISOR_HOST, port), handler_class)
|
|
318
311
|
log_daemon(f"Master Supervisor listening on http://{DEFAULT_SUPERVISOR_HOST}:{port}")
|
|
@@ -332,6 +325,7 @@ def run_master_daemon(port: int = DEFAULT_SUPERVISOR_PORT):
|
|
|
332
325
|
|
|
333
326
|
|
|
334
327
|
if __name__ == "__main__":
|
|
335
|
-
port = int(sys.argv[1]) if len(sys.argv) > 1 else DEFAULT_SUPERVISOR_PORT
|
|
336
|
-
|
|
328
|
+
port = int(sys.argv[1]) if len(sys.argv) > 1 and sys.argv[1].isdigit() else DEFAULT_SUPERVISOR_PORT
|
|
329
|
+
token = sys.argv[2] if len(sys.argv) > 2 else None
|
|
330
|
+
run_master_daemon(port=port, token=token)
|
|
337
331
|
|
|
@@ -8,7 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import os
|
|
10
10
|
import sys
|
|
11
|
-
from typing import List
|
|
11
|
+
from typing import List, Optional
|
|
12
12
|
|
|
13
13
|
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
14
14
|
if PROJECT_ROOT not in sys.path:
|
|
@@ -19,9 +19,9 @@ from supervisor.client import SupervisorClient
|
|
|
19
19
|
from supervisor.contracts import SupervisorActionRequest, SupervisorActionResponse
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
def run_interactive_shell(host: str = "127.0.0.1", port: int = 9333):
|
|
22
|
+
def run_interactive_shell(host: str = "127.0.0.1", port: int = 9333, token: Optional[str] = None):
|
|
23
23
|
"""Runs the interactive REPL session."""
|
|
24
|
-
client = SupervisorClient(base_url=f"http://{host}:{port}")
|
|
24
|
+
client = SupervisorClient(base_url=f"http://{host}:{port}", token=token)
|
|
25
25
|
try:
|
|
26
26
|
client.ensure_running()
|
|
27
27
|
except Exception as exc:
|
|
@@ -253,6 +253,7 @@ class OperationalWorker:
|
|
|
253
253
|
|
|
254
254
|
field_svc = FieldInteractionService(page)
|
|
255
255
|
confirmed = []
|
|
256
|
+
unconfirmed = []
|
|
256
257
|
failed = []
|
|
257
258
|
|
|
258
259
|
if fields_to_set:
|
|
@@ -260,12 +261,23 @@ class OperationalWorker:
|
|
|
260
261
|
for key, val in fields_to_set:
|
|
261
262
|
success = field_svc.set_field(key, val)
|
|
262
263
|
if success:
|
|
263
|
-
|
|
264
|
-
|
|
264
|
+
# Post-fill verification: read back DOM value to confirm it was applied
|
|
265
|
+
verified = field_svc.verify_field_value(key, val)
|
|
266
|
+
if not verified:
|
|
267
|
+
# Fallback retry via direct Playwright interaction
|
|
268
|
+
field_svc._playwright_click_fallback(key, val)
|
|
269
|
+
verified = field_svc.verify_field_value(key, val)
|
|
270
|
+
|
|
271
|
+
if verified:
|
|
272
|
+
confirmed.append((key, val))
|
|
273
|
+
lines.append(f" [✓] Set & Verified '{key}' -> '{val}'")
|
|
274
|
+
else:
|
|
275
|
+
unconfirmed.append((key, val))
|
|
276
|
+
lines.append(f" [?] Set but unconfirmed '{key}' -> '{val}'")
|
|
265
277
|
else:
|
|
266
278
|
failed.append((key, val))
|
|
267
279
|
lines.append(f" [~] Unmatched/Failed '{key}' -> '{val}'")
|
|
268
|
-
lines.append(f"[*] Fields summary: {len(confirmed)}
|
|
280
|
+
lines.append(f"[*] Fields summary: {len(confirmed)} confirmed | {len(unconfirmed)} unconfirmed | {len(failed)} skipped.")
|
|
269
281
|
|
|
270
282
|
# 4. Handle uploads
|
|
271
283
|
if req.upload_files:
|
|
@@ -349,6 +361,7 @@ class OperationalWorker:
|
|
|
349
361
|
total_tabs=len(ctx.pages) if ctx else 0,
|
|
350
362
|
tabs=coord.adapter.list_open_tabs(),
|
|
351
363
|
confirmed_fields=confirmed,
|
|
364
|
+
unconfirmed_fields=unconfirmed,
|
|
352
365
|
failed_fields=failed,
|
|
353
366
|
validation_errors=validation_errors,
|
|
354
367
|
schema=schema,
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Unit tests for Master Supervisor, Worker Process contracts, and Supervisor Client."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import pytest
|
|
5
|
+
from supervisor.contracts import SupervisorActionRequest, SupervisorActionResponse
|
|
6
|
+
from supervisor.client import SupervisorClient, DEFAULT_SUPERVISOR_URL
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_supervisor_action_request_roundtrip():
|
|
10
|
+
"""Verify SupervisorActionRequest serialization and deserialization."""
|
|
11
|
+
req = SupervisorActionRequest(
|
|
12
|
+
action="apply",
|
|
13
|
+
url="https://example.com/login",
|
|
14
|
+
fill_arguments=["username=testuser", "password=secret"],
|
|
15
|
+
press_buttons=["Sign In"],
|
|
16
|
+
submit=True,
|
|
17
|
+
screenshot_path="screenshot.png",
|
|
18
|
+
tab_index=1,
|
|
19
|
+
auto_inspect=False,
|
|
20
|
+
)
|
|
21
|
+
data = req.to_dict()
|
|
22
|
+
assert data["action"] == "apply"
|
|
23
|
+
assert data["url"] == "https://example.com/login"
|
|
24
|
+
assert data["fill_arguments"] == ["username=testuser", "password=secret"]
|
|
25
|
+
assert data["press_buttons"] == ["Sign In"]
|
|
26
|
+
assert data["submit"] is True
|
|
27
|
+
assert data["screenshot_path"] == "screenshot.png"
|
|
28
|
+
assert data["tab_index"] == 1
|
|
29
|
+
assert data["auto_inspect"] is False
|
|
30
|
+
|
|
31
|
+
reconstructed = SupervisorActionRequest.from_dict(data)
|
|
32
|
+
assert reconstructed == req
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_supervisor_action_response_roundtrip():
|
|
36
|
+
"""Verify SupervisorActionResponse serialization and deserialization."""
|
|
37
|
+
resp = SupervisorActionResponse(
|
|
38
|
+
success=True,
|
|
39
|
+
action="apply",
|
|
40
|
+
message="Success",
|
|
41
|
+
browser_pid=1234,
|
|
42
|
+
worker_pid=5678,
|
|
43
|
+
active_tab_index=0,
|
|
44
|
+
active_url="https://example.com/dashboard",
|
|
45
|
+
active_title="Dashboard",
|
|
46
|
+
total_tabs=1,
|
|
47
|
+
tabs=[{"index": 0, "title": "Dashboard", "url": "https://example.com/dashboard", "is_active": True}],
|
|
48
|
+
confirmed_fields=[("username", "testuser")],
|
|
49
|
+
failed_fields=[],
|
|
50
|
+
validation_errors=[],
|
|
51
|
+
output_lines=["[✓] Form submitted"],
|
|
52
|
+
)
|
|
53
|
+
data = resp.to_dict()
|
|
54
|
+
assert data["success"] is True
|
|
55
|
+
assert data["browser_pid"] == 1234
|
|
56
|
+
assert data["worker_pid"] == 5678
|
|
57
|
+
assert len(data["tabs"]) == 1
|
|
58
|
+
assert data["output_lines"] == ["[✓] Form submitted"]
|
|
59
|
+
|
|
60
|
+
reconstructed = SupervisorActionResponse.from_dict(data)
|
|
61
|
+
assert reconstructed == resp
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_supervisor_client_default_url():
|
|
65
|
+
"""Verify SupervisorClient initialization with default and custom URLs."""
|
|
66
|
+
client_default = SupervisorClient()
|
|
67
|
+
assert client_default.base_url == DEFAULT_SUPERVISOR_URL
|
|
68
|
+
|
|
69
|
+
client_custom = SupervisorClient(base_url="http://127.0.0.1:9555/")
|
|
70
|
+
assert client_custom.base_url == "http://127.0.0.1:9555"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_supervisor_action_request_defaults():
|
|
74
|
+
"""Verify default values of SupervisorActionRequest."""
|
|
75
|
+
req = SupervisorActionRequest(action="inspect")
|
|
76
|
+
assert req.action == "inspect"
|
|
77
|
+
assert req.url is None
|
|
78
|
+
assert req.fill_arguments == []
|
|
79
|
+
assert req.press_buttons == []
|
|
80
|
+
assert req.upload_files == []
|
|
81
|
+
assert req.submit is False
|
|
82
|
+
assert req.auto_inspect is True
|
|
83
|
+
assert req.timeout_minutes == 30
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_supervisor_spawn_and_restart_worker(monkeypatch):
|
|
87
|
+
"""Verify MasterSupervisor._spawn_worker and restart_worker lifecycle with start_new_session."""
|
|
88
|
+
from supervisor.master_daemon import MasterSupervisor
|
|
89
|
+
import subprocess
|
|
90
|
+
import sys
|
|
91
|
+
|
|
92
|
+
sup = MasterSupervisor(port=9777, token="dummy-token")
|
|
93
|
+
sup._is_running = False # Stop watchdog thread
|
|
94
|
+
|
|
95
|
+
spawn_calls = []
|
|
96
|
+
|
|
97
|
+
class DummyProc:
|
|
98
|
+
def __init__(self, pid):
|
|
99
|
+
self.pid = pid
|
|
100
|
+
def poll(self):
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
def fake_popen(cmd, **kwargs):
|
|
104
|
+
spawn_calls.append((cmd, kwargs))
|
|
105
|
+
return DummyProc(pid=1000 + len(spawn_calls))
|
|
106
|
+
|
|
107
|
+
monkeypatch.setattr(subprocess, "Popen", fake_popen)
|
|
108
|
+
|
|
109
|
+
# Test initial spawn via _ensure_worker
|
|
110
|
+
proc1 = sup._ensure_worker()
|
|
111
|
+
assert proc1.pid == 1001
|
|
112
|
+
assert len(spawn_calls) == 1
|
|
113
|
+
cmd1, kwargs1 = spawn_calls[0]
|
|
114
|
+
assert kwargs1["env"]["WEBPILOT_SUPERVISOR_TOKEN"] == "dummy-token"
|
|
115
|
+
if sys.platform != "win32":
|
|
116
|
+
assert kwargs1.get("start_new_session") is True
|
|
117
|
+
|
|
118
|
+
# Test restart_worker
|
|
119
|
+
terminated = []
|
|
120
|
+
monkeypatch.setattr(sup, "terminate_worker", lambda: terminated.append(True))
|
|
121
|
+
resp = sup.restart_worker()
|
|
122
|
+
|
|
123
|
+
assert resp.success is True
|
|
124
|
+
assert resp.action == "restart_worker"
|
|
125
|
+
assert resp.worker_pid == 1002
|
|
126
|
+
assert len(terminated) == 1
|
|
127
|
+
assert len(spawn_calls) == 2
|
|
128
|
+
cmd2, kwargs2 = spawn_calls[1]
|
|
129
|
+
# Verify that restart_worker uses the unified _spawn_worker
|
|
130
|
+
if sys.platform != "win32":
|
|
131
|
+
assert kwargs2.get("start_new_session") is True
|
|
132
|
+
assert kwargs2["env"]["WEBPILOT_SUPERVISOR_TOKEN"] == "dummy-token"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_worker_process_field_verification(monkeypatch):
|
|
136
|
+
"""Verify worker_process calls verify_field_value and populates confirmed and unconfirmed fields."""
|
|
137
|
+
from supervisor.worker_process import OperationalWorker
|
|
138
|
+
from unittest.mock import MagicMock
|
|
139
|
+
|
|
140
|
+
worker = OperationalWorker()
|
|
141
|
+
|
|
142
|
+
mock_coord = MagicMock()
|
|
143
|
+
mock_coord.page.url = "https://example.com/form"
|
|
144
|
+
mock_coord.page.title.return_value = "Test Form"
|
|
145
|
+
mock_coord.page.is_closed.return_value = False
|
|
146
|
+
mock_coord.page.locator.return_value.count.return_value = 0
|
|
147
|
+
mock_coord.context.pages = [mock_coord.page]
|
|
148
|
+
worker.coordinator = mock_coord
|
|
149
|
+
monkeypatch.setattr(worker, "_ensure_session", lambda req: mock_coord)
|
|
150
|
+
|
|
151
|
+
# Mock FieldInteractionService
|
|
152
|
+
mock_field_svc = MagicMock()
|
|
153
|
+
mock_field_svc.set_field.side_effect = lambda k, v: k != "skipped_field"
|
|
154
|
+
mock_field_svc.verify_field_value.side_effect = lambda k, v: k == "verified_field"
|
|
155
|
+
|
|
156
|
+
monkeypatch.setattr("supervisor.worker_process.FieldInteractionService", lambda page: mock_field_svc)
|
|
157
|
+
monkeypatch.setattr("supervisor.worker_process.AuthNavigationService", lambda p, c: MagicMock())
|
|
158
|
+
monkeypatch.setattr("supervisor.worker_process.FormValidator", lambda p: MagicMock(get_validation_errors=lambda: []))
|
|
159
|
+
monkeypatch.setattr("supervisor.worker_process.InspectionService", lambda p: MagicMock())
|
|
160
|
+
|
|
161
|
+
req = SupervisorActionRequest(
|
|
162
|
+
action="apply",
|
|
163
|
+
url="https://example.com/form",
|
|
164
|
+
fill_arguments=[
|
|
165
|
+
"verified_field=value1",
|
|
166
|
+
"unverified_field=value2",
|
|
167
|
+
"skipped_field=value3",
|
|
168
|
+
],
|
|
169
|
+
auto_inspect=False,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
resp = worker._handle_apply(req)
|
|
173
|
+
|
|
174
|
+
assert resp.success is True
|
|
175
|
+
assert resp.confirmed_fields == [("verified_field", "value1")]
|
|
176
|
+
assert resp.unconfirmed_fields == [("unverified_field", "value2")]
|
|
177
|
+
assert resp.failed_fields == [("skipped_field", "value3")]
|
|
178
|
+
assert mock_field_svc.verify_field_value.called
|
|
179
|
+
|
|
@@ -87,3 +87,22 @@ def test_supervisor_http_auth_enforcement():
|
|
|
87
87
|
finally:
|
|
88
88
|
server.shutdown()
|
|
89
89
|
server.server_close()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_cli_token_argument_and_client_propagation():
|
|
93
|
+
"""Verify CLI --token flag parsing and propagation to SupervisorClient."""
|
|
94
|
+
import argparse
|
|
95
|
+
from cli import get_supervisor_client
|
|
96
|
+
|
|
97
|
+
# 1. Custom token provided in args
|
|
98
|
+
args_with_token = argparse.Namespace(host="127.0.0.1", port=9333, token="my-custom-token-xyz")
|
|
99
|
+
client = get_supervisor_client(args_with_token)
|
|
100
|
+
assert client.token == "my-custom-token-xyz"
|
|
101
|
+
headers = client._get_headers()
|
|
102
|
+
assert headers["X-Supervisor-Token"] == "my-custom-token-xyz"
|
|
103
|
+
assert headers["Authorization"] == "Bearer my-custom-token-xyz"
|
|
104
|
+
|
|
105
|
+
# 2. No token in args -> falls back to load_supervisor_token
|
|
106
|
+
args_no_token = argparse.Namespace(host="127.0.0.1", port=9333, token=None)
|
|
107
|
+
client_default = get_supervisor_client(args_no_token)
|
|
108
|
+
assert client_default.base_url == "http://127.0.0.1:9333"
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
"""Unit tests for Master Supervisor, Worker Process contracts, and Supervisor Client."""
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import pytest
|
|
5
|
-
from supervisor.contracts import SupervisorActionRequest, SupervisorActionResponse
|
|
6
|
-
from supervisor.client import SupervisorClient, DEFAULT_SUPERVISOR_URL
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def test_supervisor_action_request_roundtrip():
|
|
10
|
-
"""Verify SupervisorActionRequest serialization and deserialization."""
|
|
11
|
-
req = SupervisorActionRequest(
|
|
12
|
-
action="apply",
|
|
13
|
-
url="https://example.com/login",
|
|
14
|
-
fill_arguments=["username=testuser", "password=secret"],
|
|
15
|
-
press_buttons=["Sign In"],
|
|
16
|
-
submit=True,
|
|
17
|
-
screenshot_path="screenshot.png",
|
|
18
|
-
tab_index=1,
|
|
19
|
-
auto_inspect=False,
|
|
20
|
-
)
|
|
21
|
-
data = req.to_dict()
|
|
22
|
-
assert data["action"] == "apply"
|
|
23
|
-
assert data["url"] == "https://example.com/login"
|
|
24
|
-
assert data["fill_arguments"] == ["username=testuser", "password=secret"]
|
|
25
|
-
assert data["press_buttons"] == ["Sign In"]
|
|
26
|
-
assert data["submit"] is True
|
|
27
|
-
assert data["screenshot_path"] == "screenshot.png"
|
|
28
|
-
assert data["tab_index"] == 1
|
|
29
|
-
assert data["auto_inspect"] is False
|
|
30
|
-
|
|
31
|
-
reconstructed = SupervisorActionRequest.from_dict(data)
|
|
32
|
-
assert reconstructed == req
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def test_supervisor_action_response_roundtrip():
|
|
36
|
-
"""Verify SupervisorActionResponse serialization and deserialization."""
|
|
37
|
-
resp = SupervisorActionResponse(
|
|
38
|
-
success=True,
|
|
39
|
-
action="apply",
|
|
40
|
-
message="Success",
|
|
41
|
-
browser_pid=1234,
|
|
42
|
-
worker_pid=5678,
|
|
43
|
-
active_tab_index=0,
|
|
44
|
-
active_url="https://example.com/dashboard",
|
|
45
|
-
active_title="Dashboard",
|
|
46
|
-
total_tabs=1,
|
|
47
|
-
tabs=[{"index": 0, "title": "Dashboard", "url": "https://example.com/dashboard", "is_active": True}],
|
|
48
|
-
confirmed_fields=[("username", "testuser")],
|
|
49
|
-
failed_fields=[],
|
|
50
|
-
validation_errors=[],
|
|
51
|
-
output_lines=["[✓] Form submitted"],
|
|
52
|
-
)
|
|
53
|
-
data = resp.to_dict()
|
|
54
|
-
assert data["success"] is True
|
|
55
|
-
assert data["browser_pid"] == 1234
|
|
56
|
-
assert data["worker_pid"] == 5678
|
|
57
|
-
assert len(data["tabs"]) == 1
|
|
58
|
-
assert data["output_lines"] == ["[✓] Form submitted"]
|
|
59
|
-
|
|
60
|
-
reconstructed = SupervisorActionResponse.from_dict(data)
|
|
61
|
-
assert reconstructed == resp
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def test_supervisor_client_default_url():
|
|
65
|
-
"""Verify SupervisorClient initialization with default and custom URLs."""
|
|
66
|
-
client_default = SupervisorClient()
|
|
67
|
-
assert client_default.base_url == DEFAULT_SUPERVISOR_URL
|
|
68
|
-
|
|
69
|
-
client_custom = SupervisorClient(base_url="http://127.0.0.1:9555/")
|
|
70
|
-
assert client_custom.base_url == "http://127.0.0.1:9555"
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def test_supervisor_action_request_defaults():
|
|
74
|
-
"""Verify default values of SupervisorActionRequest."""
|
|
75
|
-
req = SupervisorActionRequest(action="inspect")
|
|
76
|
-
assert req.action == "inspect"
|
|
77
|
-
assert req.url is None
|
|
78
|
-
assert req.fill_arguments == []
|
|
79
|
-
assert req.press_buttons == []
|
|
80
|
-
assert req.upload_files == []
|
|
81
|
-
assert req.submit is False
|
|
82
|
-
assert req.auto_inspect is True
|
|
83
|
-
assert req.timeout_minutes == 30
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{webpilot_engine-2.0.6 → webpilot_engine-2.0.7}/webpilot_engine.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|