aetherius 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. aetherius/__init__.py +42 -0
  2. aetherius/__main__.py +12 -0
  3. aetherius/_contracts/__init__.py +0 -0
  4. aetherius/_contracts/blueprint.schema.json +144 -0
  5. aetherius/acts/__init__.py +1 -0
  6. aetherius/acts/_shared.py +61 -0
  7. aetherius/acts/continuum/__init__.py +7 -0
  8. aetherius/acts/continuum/actions.py +149 -0
  9. aetherius/acts/continuum/bridge.py +159 -0
  10. aetherius/acts/continuum/browser.py +164 -0
  11. aetherius/acts/continuum/debug_overlay.py +65 -0
  12. aetherius/acts/continuum/driver.py +127 -0
  13. aetherius/acts/continuum/human_actions.py +74 -0
  14. aetherius/acts/oracle/__init__.py +1 -0
  15. aetherius/acts/oracle/driver.py +1 -0
  16. aetherius/acts/oracle/locator.py +1 -0
  17. aetherius/acts/oracle/model.py +1 -0
  18. aetherius/acts/oracle/perception.py +1 -0
  19. aetherius/acts/phantom/__init__.py +1 -0
  20. aetherius/acts/phantom/driver.py +1 -0
  21. aetherius/acts/phantom/loop.py +1 -0
  22. aetherius/acts/phantom/memory.py +1 -0
  23. aetherius/acts/phantom/perception.py +1 -0
  24. aetherius/acts/phantom/planner.py +1 -0
  25. aetherius/acts/vector/__init__.py +1 -0
  26. aetherius/acts/vector/auth.py +108 -0
  27. aetherius/acts/vector/client.py +129 -0
  28. aetherius/acts/vector/driver.py +128 -0
  29. aetherius/builder/__init__.py +40 -0
  30. aetherius/builder/catalog.py +93 -0
  31. aetherius/builder/factory.py +243 -0
  32. aetherius/builder/templates.py +150 -0
  33. aetherius/builder/validation.py +120 -0
  34. aetherius/cli.py +236 -0
  35. aetherius/config/__init__.py +1 -0
  36. aetherius/config/secrets.py +71 -0
  37. aetherius/config/settings.py +44 -0
  38. aetherius/console/__init__.py +1 -0
  39. aetherius/console/app.py +66 -0
  40. aetherius/console/console.tcss +97 -0
  41. aetherius/console/daemon_control.py +83 -0
  42. aetherius/console/run_bridge.py +43 -0
  43. aetherius/console/screens/__init__.py +1 -0
  44. aetherius/console/screens/_pending.py +38 -0
  45. aetherius/console/screens/builder/__init__.py +1 -0
  46. aetherius/console/screens/builder/act_picker.py +75 -0
  47. aetherius/console/screens/builder/io_editor.py +167 -0
  48. aetherius/console/screens/builder/options_editor.py +132 -0
  49. aetherius/console/screens/builder/preview.py +57 -0
  50. aetherius/console/screens/builder/screen.py +235 -0
  51. aetherius/console/screens/builder/step_editor.py +249 -0
  52. aetherius/console/screens/catalog.py +52 -0
  53. aetherius/console/screens/home.py +90 -0
  54. aetherius/console/screens/library.py +124 -0
  55. aetherius/console/screens/library_scan.py +99 -0
  56. aetherius/console/screens/recorder.py +193 -0
  57. aetherius/console/screens/runs.py +138 -0
  58. aetherius/console/screens/sessions.py +14 -0
  59. aetherius/console/screens/settings.py +132 -0
  60. aetherius/console/screenshots.py +173 -0
  61. aetherius/console/theme.py +114 -0
  62. aetherius/console/widgets/__init__.py +1 -0
  63. aetherius/console/widgets/event_log.py +44 -0
  64. aetherius/console/widgets/form.py +101 -0
  65. aetherius/console/widgets/json_preview.py +18 -0
  66. aetherius/console/widgets/run_summary.py +82 -0
  67. aetherius/core/__init__.py +1 -0
  68. aetherius/core/actions/__init__.py +1 -0
  69. aetherius/core/actions/base.py +99 -0
  70. aetherius/core/actions/data.py +126 -0
  71. aetherius/core/actions/flow.py +76 -0
  72. aetherius/core/actions/interaction.py +109 -0
  73. aetherius/core/actions/navigation.py +32 -0
  74. aetherius/core/actions/registry.py +70 -0
  75. aetherius/core/actions/spec.py +42 -0
  76. aetherius/core/blueprint/__init__.py +1 -0
  77. aetherius/core/blueprint/loader.py +82 -0
  78. aetherius/core/blueprint/models.py +81 -0
  79. aetherius/core/blueprint/template.py +101 -0
  80. aetherius/core/blueprint/validator.py +45 -0
  81. aetherius/core/driver.py +36 -0
  82. aetherius/core/errors.py +134 -0
  83. aetherius/core/events/__init__.py +1 -0
  84. aetherius/core/events/bus.py +28 -0
  85. aetherius/core/events/models.py +29 -0
  86. aetherius/core/events/sinks.py +55 -0
  87. aetherius/core/extraction/__init__.py +1 -0
  88. aetherius/core/extraction/html_extractor.py +52 -0
  89. aetherius/core/extraction/json_extractor.py +125 -0
  90. aetherius/core/runtime/__init__.py +1 -0
  91. aetherius/core/runtime/context.py +61 -0
  92. aetherius/core/runtime/engine.py +188 -0
  93. aetherius/core/runtime/result.py +39 -0
  94. aetherius/core/runtime/selector.py +1 -0
  95. aetherius/models/__init__.py +1 -0
  96. aetherius/models/registry.py +1 -0
  97. aetherius/models/store/.gitkeep +0 -0
  98. aetherius/recorder/__init__.py +12 -0
  99. aetherius/recorder/_capture_js.py +80 -0
  100. aetherius/recorder/_gesture_js.py +40 -0
  101. aetherius/recorder/_names.py +20 -0
  102. aetherius/recorder/_overlay_js.py +262 -0
  103. aetherius/recorder/_playwright.py +49 -0
  104. aetherius/recorder/_selector_js.py +148 -0
  105. aetherius/recorder/_transform.py +214 -0
  106. aetherius/recorder/_vector_js.py +174 -0
  107. aetherius/recorder/base.py +92 -0
  108. aetherius/recorder/blueprint_recorder.py +72 -0
  109. aetherius/recorder/capture.py +59 -0
  110. aetherius/recorder/continuum_backend.py +119 -0
  111. aetherius/recorder/gesture_recorder.py +193 -0
  112. aetherius/recorder/selector_synth.py +83 -0
  113. aetherius/recorder/session.py +101 -0
  114. aetherius/recorder/vector_backend.py +158 -0
  115. aetherius/server/__init__.py +12 -0
  116. aetherius/server/app.py +37 -0
  117. aetherius/server/config.py +25 -0
  118. aetherius/server/deps.py +50 -0
  119. aetherius/server/jobs.py +155 -0
  120. aetherius/server/routes/__init__.py +1 -0
  121. aetherius/server/routes/blueprints.py +45 -0
  122. aetherius/server/routes/recorder.py +25 -0
  123. aetherius/server/routes/runs.py +49 -0
  124. aetherius/server/routes/stream.py +53 -0
  125. aetherius/server/schemas.py +78 -0
  126. aetherius/stealth/__init__.py +1 -0
  127. aetherius/stealth/fingerprint/__init__.py +1 -0
  128. aetherius/stealth/fingerprint/patch.py +38 -0
  129. aetherius/stealth/fingerprint/profile.py +95 -0
  130. aetherius/stealth/gestures/__init__.py +1 -0
  131. aetherius/stealth/gestures/data/human_library.json +7107 -0
  132. aetherius/stealth/gestures/library.py +126 -0
  133. aetherius/stealth/gestures/seed.py +97 -0
  134. aetherius/stealth/humanizer/__init__.py +1 -0
  135. aetherius/stealth/humanizer/input.py +89 -0
  136. aetherius/stealth/humanizer/keyboard.py +75 -0
  137. aetherius/stealth/humanizer/mouse.py +153 -0
  138. aetherius/stealth/humanizer/scroll.py +55 -0
  139. aetherius/stealth/humanizer/timing.py +54 -0
  140. aetherius/stealth/ml/__init__.py +1 -0
  141. aetherius/stealth/ml/fingerprint_model.py +1 -0
  142. aetherius/stealth/ml/motion_model.py +1 -0
  143. aetherius/stealth/policy.py +133 -0
  144. aetherius/stealth/session/__init__.py +1 -0
  145. aetherius/stealth/session/store.py +35 -0
  146. aetherius/stealth/session/warmup.py +52 -0
  147. aetherius/version.py +3 -0
  148. aetherius-0.2.0.data/data/aetherius/_contracts/__init__.py +0 -0
  149. aetherius-0.2.0.data/data/aetherius/_contracts/blueprint.schema.json +144 -0
  150. aetherius-0.2.0.dist-info/METADATA +606 -0
  151. aetherius-0.2.0.dist-info/RECORD +154 -0
  152. aetherius-0.2.0.dist-info/WHEEL +4 -0
  153. aetherius-0.2.0.dist-info/entry_points.txt +2 -0
  154. aetherius-0.2.0.dist-info/licenses/LICENSE +11 -0
@@ -0,0 +1,164 @@
1
+ """Playwright browser lifecycle: launch, contexts, headed and headless modes, debug options.
2
+
3
+ Wraps the synchronous Playwright API. The engine (and therefore this driver) is synchronous, so the
4
+ sync API is the natural fit: no event loop to juggle inside the driver. Playwright is imported
5
+ lazily so ``import aetherius`` never pulls it in; a missing extra surfaces as a typed DependencyError.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import time
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from ...core.errors import DependencyError
15
+ from ...stealth.fingerprint.patch import FINGERPRINT_PATCH_JS
16
+ from ...stealth.fingerprint.profile import get_profile
17
+ from ...stealth.humanizer.input import HumanInput
18
+ from ...stealth.policy import OFF, StealthPolicy
19
+ from .debug_overlay import DEBUG_OVERLAY_JS
20
+ from .human_actions import humanized_actions
21
+
22
+ # Slow-motion delay (ms) inserted before every action when debug is on, so a human can follow along.
23
+ _DEBUG_SLOW_MO_MS = 500
24
+ # How long the window lingers after the last step (or a failure) in debug, so it does not just
25
+ # flash and vanish before the final state can be read.
26
+ _DEBUG_LINGER_MS = 2500
27
+
28
+
29
+ def _import_playwright() -> Any:
30
+ """Import the sync Playwright entry point, or raise a typed, actionable error."""
31
+ try:
32
+ from playwright.sync_api import sync_playwright
33
+ except ImportError as exc: # extra [browser] not installed
34
+ raise DependencyError(
35
+ "Act II (Continuum) requires Playwright. Install it with:\n"
36
+ ' pip install "aetherius[browser]" && playwright install chromium',
37
+ extra="browser",
38
+ ) from exc
39
+ return sync_playwright
40
+
41
+
42
+ class BrowserSession:
43
+ """Owns a Playwright instance, context and page for the duration of a run.
44
+
45
+ An ephemeral context by default; a *persistent* context (reused cookies/cache/history) when a
46
+ profile directory is supplied. ``debug`` opens a headed window with slow-motion so a step can be
47
+ watched. ``stealth`` is the assembled :class:`StealthPolicy`: an active one wears a fingerprint
48
+ profile (context options + init patches) and exposes a :class:`HumanInput` for the driver to
49
+ route interactive actions through.
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ *,
55
+ debug: bool = False,
56
+ timeout_ms: int = 30_000,
57
+ profile_dir: Path | None = None,
58
+ stealth: StealthPolicy = OFF,
59
+ ) -> None:
60
+ self._debug = debug
61
+ self._timeout_ms = timeout_ms
62
+ self._profile_dir = profile_dir
63
+ self._stealth = stealth
64
+ self._pw: Any = None
65
+ self._browser: Any = None
66
+ self._context: Any = None
67
+ self._page: Any = None
68
+ self._human: HumanInput | None = None
69
+
70
+ def start(self) -> None:
71
+ """Launch the browser and open the working page. Idempotent per session."""
72
+ sync_playwright = _import_playwright()
73
+ self._pw = sync_playwright().start()
74
+
75
+ headless = not self._debug
76
+ slow_mo = self._slow_mo_ms()
77
+ context_options = self._context_options()
78
+
79
+ if self._profile_dir is not None:
80
+ self._context = self._pw.chromium.launch_persistent_context(
81
+ str(self._profile_dir),
82
+ headless=headless,
83
+ slow_mo=slow_mo,
84
+ **context_options,
85
+ )
86
+ pages = self._context.pages
87
+ self._page = pages[0] if pages else self._context.new_page()
88
+ else:
89
+ self._browser = self._pw.chromium.launch(headless=headless, slow_mo=slow_mo)
90
+ self._context = self._browser.new_context(**context_options)
91
+ self._page = self._context.new_page()
92
+
93
+ self._context.set_default_timeout(self._timeout_ms)
94
+ self._apply_stealth()
95
+
96
+ if self._debug:
97
+ # Visible cursor + red click ripple, re-installed on every navigation.
98
+ self._context.add_init_script(DEBUG_OVERLAY_JS)
99
+
100
+ def _slow_mo_ms(self) -> int:
101
+ """Debug slow-motion delay, but 0 whenever inputs are humanized.
102
+
103
+ slow_mo delays *every* Playwright op. That is fine for plain actions, but the humanizer
104
+ issues dozens of mouse.move/wheel calls per gesture and supplies its own realistic timing;
105
+ stacking slow_mo on top would shred each gesture into a slow, ugly stutter. So when discretion
106
+ humanizes inputs, the humanizer owns the pacing and slow_mo stays off, even in debug.
107
+ """
108
+ if self._debug and not humanized_actions(self._stealth):
109
+ return _DEBUG_SLOW_MO_MS
110
+ return 0
111
+
112
+ def _context_options(self) -> dict[str, Any]:
113
+ """Playwright context options for the active fingerprint profile (empty when none)."""
114
+ if self._stealth.fingerprint is None:
115
+ return {}
116
+ return get_profile(self._stealth.fingerprint).context_options()
117
+
118
+ def _apply_stealth(self) -> None:
119
+ """Inject the fingerprint patches and, if inputs are humanized, build the HumanInput facade."""
120
+ if not self._stealth.is_active:
121
+ return
122
+ self._context.add_init_script(FINGERPRINT_PATCH_JS)
123
+ if self._stealth.fingerprint is not None:
124
+ self._context.add_init_script(get_profile(self._stealth.fingerprint).init_script())
125
+ if humanized_actions(self._stealth):
126
+ self._human = HumanInput(self._page, self._stealth)
127
+
128
+ @property
129
+ def human(self) -> HumanInput | None:
130
+ """The humanized input facade when discretion humanizes inputs, else ``None``."""
131
+ return self._human
132
+
133
+ @property
134
+ def page(self) -> Any:
135
+ """The active Playwright page. Valid only between ``start`` and ``close``."""
136
+ if self._page is None:
137
+ raise RuntimeError("BrowserSession.page accessed before start() or after close().")
138
+ return self._page
139
+
140
+ def close(self) -> None:
141
+ """Tear down page, context, browser and Playwright, tolerating partial startup.
142
+
143
+ In debug mode, linger briefly first so the final page (or the point of failure) stays on
144
+ screen instead of vanishing the instant the run ends.
145
+ """
146
+ if self._debug and self._page is not None:
147
+ time.sleep(_DEBUG_LINGER_MS / 1000)
148
+
149
+ for closer in (
150
+ self._context,
151
+ self._browser,
152
+ ):
153
+ if closer is not None:
154
+ try:
155
+ closer.close()
156
+ except Exception: # teardown must never mask the original run error
157
+ pass
158
+ if self._pw is not None:
159
+ try:
160
+ self._pw.stop()
161
+ except Exception:
162
+ pass
163
+ self._page = self._context = self._browser = self._pw = None
164
+ self._human = None
@@ -0,0 +1,65 @@
1
+ """Client-side debug overlay: a visible cursor and a red click ripple.
2
+
3
+ Injected via Playwright's ``add_init_script`` when ``options.debug`` is on, so it re-installs on
4
+ every navigation. It listens to the real mouse events Playwright dispatches, so the cursor tracks
5
+ where the bot moves and a red ripple marks every click. Purely a debug aid — never loaded otherwise,
6
+ and unrelated to the discretion layer (which does the opposite: hide automation).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ # A self-invoking script (add_init_script evaluates verbatim, it does not call a returned function).
12
+ # Guarded by a window flag so repeated injection on the same document is a no-op.
13
+ DEBUG_OVERLAY_JS = """
14
+ (() => {
15
+ if (window.__aetheriusDebugOverlay) return;
16
+ window.__aetheriusDebugOverlay = true;
17
+ const install = () => {
18
+ const root = document.documentElement;
19
+ if (!root) return;
20
+ const style = document.createElement('style');
21
+ style.textContent = `
22
+ .__ae-cursor {
23
+ position: fixed; z-index: 2147483647; pointer-events: none;
24
+ width: 20px; height: 20px; margin: -10px 0 0 -10px; opacity: 0;
25
+ border: 2px solid rgba(224,48,48,0.95); border-radius: 50%;
26
+ background: rgba(224,48,48,0.20);
27
+ box-shadow: 0 0 8px 2px rgba(224,48,48,0.55);
28
+ transition: opacity 0.25s linear;
29
+ }
30
+ .__ae-ripple {
31
+ position: fixed; z-index: 2147483647; pointer-events: none;
32
+ width: 12px; height: 12px; margin: -6px 0 0 -6px; border-radius: 50%;
33
+ background: rgba(224,48,48,0.6);
34
+ animation: __ae-ripple-anim 0.65s ease-out forwards;
35
+ }
36
+ @keyframes __ae-ripple-anim {
37
+ 0% { transform: scale(1); opacity: 0.9; }
38
+ 100% { transform: scale(6); opacity: 0; }
39
+ }
40
+ `;
41
+ root.appendChild(style);
42
+ const cursor = document.createElement('div');
43
+ cursor.className = '__ae-cursor';
44
+ root.appendChild(cursor);
45
+ document.addEventListener('mousemove', (e) => {
46
+ cursor.style.opacity = '1';
47
+ cursor.style.left = e.clientX + 'px';
48
+ cursor.style.top = e.clientY + 'px';
49
+ }, true);
50
+ document.addEventListener('mousedown', (e) => {
51
+ const ripple = document.createElement('div');
52
+ ripple.className = '__ae-ripple';
53
+ ripple.style.left = e.clientX + 'px';
54
+ ripple.style.top = e.clientY + 'px';
55
+ root.appendChild(ripple);
56
+ setTimeout(() => ripple.remove(), 700);
57
+ }, true);
58
+ };
59
+ if (document.readyState === 'loading') {
60
+ document.addEventListener('DOMContentLoaded', install);
61
+ } else {
62
+ install();
63
+ }
64
+ })();
65
+ """
@@ -0,0 +1,127 @@
1
+ """Continuum driver: follows the Blueprint literally against a real browser page.
2
+
3
+ Act II. Dispatches each step to a Playwright page operation (actions.py), a DOM read
4
+ (bridge.py), an artifact producer (screenshot), or an Act-agnostic handler (SharedActionsMixin).
5
+ Synchronous throughout, matching the engine; Playwright is imported lazily by BrowserSession.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any, Callable
11
+
12
+ from .._shared import SharedActionsMixin
13
+ from ...core.blueprint.models import StepModel
14
+ from ...core.errors import ActionError
15
+ from ...core.events.bus import EventBus
16
+ from ...core.events.models import EventType, RunEvent
17
+ from ...core.runtime.context import RunContext
18
+ from ...stealth.policy import StealthPolicy, build_policy
19
+ from ...stealth.session.store import resolve_profile_dir, run_dir
20
+ from .actions import PAGE_ACTIONS, _locator
21
+ from .bridge import evaluate, extract, wait_for
22
+ from .browser import BrowserSession
23
+ from .human_actions import HUMAN_ACTIONS, humanized_actions
24
+
25
+
26
+ class ContinuumDriver(SharedActionsMixin):
27
+ act = "continuum"
28
+
29
+ def __init__(self) -> None:
30
+ self._session: BrowserSession | None = None
31
+ self._policy: StealthPolicy | None = None
32
+ self._humanized: frozenset[str] = frozenset()
33
+
34
+ def setup(self, ctx: RunContext) -> None:
35
+ opts = ctx.blueprint.options
36
+ session_opts = opts.session
37
+ profile_dir = None
38
+ if session_opts is not None and session_opts.persist:
39
+ profile_dir = resolve_profile_dir(session_opts.profile)
40
+ self._policy = build_policy(opts.stealth)
41
+ self._humanized = humanized_actions(self._policy)
42
+ self._session = BrowserSession(
43
+ debug=opts.debug,
44
+ timeout_ms=opts.timeout_ms or 30_000,
45
+ profile_dir=profile_dir,
46
+ stealth=self._policy,
47
+ )
48
+ self._session.start()
49
+
50
+ def teardown(self, ctx: RunContext) -> None:
51
+ if self._session is not None:
52
+ self._session.close()
53
+ self._session = None
54
+
55
+ def run_step(
56
+ self,
57
+ step: StepModel,
58
+ ctx: RunContext,
59
+ bus: EventBus,
60
+ renderer: Callable[[Any], Any],
61
+ ) -> dict[str, Any]:
62
+ if self._session is None:
63
+ raise ActionError("ContinuumDriver.run_step called before setup().")
64
+ page = self._session.page
65
+ params = step.extra_fields
66
+
67
+ human = self._session.human
68
+ if human is not None and step.action in self._humanized:
69
+ return HUMAN_ACTIONS[step.action](human, params, renderer)
70
+
71
+ page_action = PAGE_ACTIONS.get(step.action)
72
+ if page_action is not None:
73
+ return page_action(page, params, renderer)
74
+
75
+ match step.action:
76
+ case "extract":
77
+ return extract(page, params, renderer)
78
+ case "wait_for":
79
+ return wait_for(page, params, renderer)
80
+ case "evaluate":
81
+ return evaluate(page, params, renderer)
82
+ case "screenshot":
83
+ return self._screenshot(step, ctx, bus, renderer)
84
+ case "emit":
85
+ return self._emit(step, ctx, bus, renderer)
86
+ case "wait":
87
+ if human is not None:
88
+ human.park() # drift the cursor off the controls while idling, like a person
89
+ return self._wait(step, renderer)
90
+ case "set":
91
+ return self._set(step, renderer)
92
+ case "assert":
93
+ return self._assert(step, renderer)
94
+ case _:
95
+ raise ActionError(f"ContinuumDriver: unsupported action {step.action!r}")
96
+
97
+ def _screenshot(
98
+ self,
99
+ step: StepModel,
100
+ ctx: RunContext,
101
+ bus: EventBus,
102
+ renderer: Callable[[Any], Any],
103
+ ) -> dict[str, Any]:
104
+ assert self._session is not None
105
+ page = self._session.page
106
+ params = step.extra_fields
107
+ name = renderer(params.get("name")) or f"{step.id or 'screenshot'}.png"
108
+ path = run_dir(ctx.run_id) / str(name)
109
+
110
+ if params.get("selector"):
111
+ _locator(page, params, renderer).screenshot(path=str(path))
112
+ else:
113
+ page.screenshot(
114
+ path=str(path), full_page=bool(renderer(params.get("full_page", False)))
115
+ )
116
+
117
+ bus.emit(
118
+ RunEvent(
119
+ run_id=ctx.run_id,
120
+ type=EventType.ARTIFACT,
121
+ step_id=step.id,
122
+ message=f"screenshot: {path}",
123
+ level="info",
124
+ data={"kind": "screenshot", "path": str(path)},
125
+ )
126
+ )
127
+ return {"path": str(path)}
@@ -0,0 +1,74 @@
1
+ """Humanized variants of the interactive page actions, used when a StealthPolicy enables them.
2
+
3
+ The plain actions in ``actions.py`` stay the fast, deterministic default. When discretion is on, the
4
+ driver routes the interactive subset (click/hover/fill/type/scroll) here instead: same
5
+ ``(params, render) -> dict`` contract, but the operation runs through :class:`HumanInput` so it is
6
+ paced and moved like a person. Locator resolution is reused from ``actions.py`` — this module owns
7
+ none of Playwright's selector logic, only the choice to humanize.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any, Callable, Mapping
13
+
14
+ from ...stealth.humanizer.input import HumanInput
15
+ from ...stealth.policy import StealthPolicy
16
+ from .actions import Renderer, _locator
17
+
18
+
19
+ def _target(human: HumanInput, params: Mapping[str, Any], render: Renderer) -> Any:
20
+ return _locator(human.page, params, render)
21
+
22
+
23
+ def click(human: HumanInput, params: Mapping[str, Any], render: Renderer) -> dict[str, Any]:
24
+ human.click(_target(human, params, render))
25
+ return {}
26
+
27
+
28
+ def hover(human: HumanInput, params: Mapping[str, Any], render: Renderer) -> dict[str, Any]:
29
+ human.hover(_target(human, params, render))
30
+ return {}
31
+
32
+
33
+ def fill(human: HumanInput, params: Mapping[str, Any], render: Renderer) -> dict[str, Any]:
34
+ human.fill(_target(human, params, render), render(params.get("value", "")))
35
+ return {}
36
+
37
+
38
+ def type_text(human: HumanInput, params: Mapping[str, Any], render: Renderer) -> dict[str, Any]:
39
+ text = render(params.get("text", params.get("value", "")))
40
+ human.type(_target(human, params, render), text)
41
+ return {}
42
+
43
+
44
+ def scroll(human: HumanInput, params: Mapping[str, Any], render: Renderer) -> dict[str, Any]:
45
+ # A targeted scroll is about reaching an element (kept programmatic); a bare delta is a real
46
+ # wheel gesture and gets the eased humanized motion.
47
+ if params.get("selector"):
48
+ _locator(human.page, params, render).scroll_into_view_if_needed()
49
+ else:
50
+ human.scroll_by(float(render(params.get("dy", 0)) or 0))
51
+ return {}
52
+
53
+
54
+ HumanAction = Callable[[HumanInput, Mapping[str, Any], Renderer], dict[str, Any]]
55
+
56
+ HUMAN_ACTIONS: dict[str, HumanAction] = {
57
+ "click": click,
58
+ "hover": hover,
59
+ "fill": fill,
60
+ "type": type_text,
61
+ "scroll": scroll,
62
+ }
63
+
64
+
65
+ def humanized_actions(policy: StealthPolicy) -> frozenset[str]:
66
+ """Which action names *policy* should route to the humanized layer (empty when none apply)."""
67
+ actions: set[str] = set()
68
+ if policy.mouse == "gestures":
69
+ actions |= {"click", "hover", "fill", "type"}
70
+ if policy.keyboard == "human":
71
+ actions |= {"fill", "type"}
72
+ if policy.scroll == "eased":
73
+ actions |= {"scroll"}
74
+ return frozenset(actions)
@@ -0,0 +1 @@
1
+ """Act III - Oracle: vision-guided browser automation with stealth as a first-class concern."""
@@ -0,0 +1 @@
1
+ """Oracle driver: locates targets by vision when selectors are brittle, acts through the stealth humanizer."""
@@ -0,0 +1 @@
1
+ """Turns a vision target description into concrete on-screen coordinates."""
@@ -0,0 +1 @@
1
+ """Loading and inference wrapper for the Oracle vision models."""
@@ -0,0 +1 @@
1
+ """Screenshot perception: runs the task-specific ONNX detector to find UI elements."""
@@ -0,0 +1 @@
1
+ """Act IV - Phantom: autonomous decisional agent. The heaviest Act."""
@@ -0,0 +1 @@
1
+ """Phantom driver: goal-driven execution, stealth by default, maximal resilience."""
@@ -0,0 +1 @@
1
+ """The perceive-reason-act loop that advances the agent toward its goal."""
@@ -0,0 +1 @@
1
+ """Working and task memory for the agent across steps."""
@@ -0,0 +1 @@
1
+ """Perception fusion: DOM, accessibility tree and vision merged into the planner state."""
@@ -0,0 +1 @@
1
+ """Decision policy: a vision-language planner (Claude by default, pluggable) that chooses the next action."""
@@ -0,0 +1 @@
1
+ """Act I - Vector: HTTP/API engine. The lightest Act."""
@@ -0,0 +1,108 @@
1
+ """Authentication strategies for Vector (Act I): NoAuth, Bearer, Basic, Cookie, CAS form-login."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Protocol, runtime_checkable
6
+
7
+ import httpx
8
+ from parsel import Selector
9
+
10
+
11
+ @runtime_checkable
12
+ class AuthStrategy(Protocol):
13
+ def prepare(self, client: httpx.Client) -> None:
14
+ """Called once before the first request (e.g., perform a login flow)."""
15
+ ...
16
+
17
+ def apply(self, request: httpx.Request) -> httpx.Request:
18
+ """Mutate or replace a request to inject auth headers/cookies. Called per-request."""
19
+ ...
20
+
21
+
22
+ class NoAuth:
23
+ def prepare(self, client: httpx.Client) -> None:
24
+ pass
25
+
26
+ def apply(self, request: httpx.Request) -> httpx.Request:
27
+ return request
28
+
29
+
30
+ class BearerAuth:
31
+ def __init__(self, token: str) -> None:
32
+ self._token = token
33
+
34
+ def prepare(self, client: httpx.Client) -> None:
35
+ pass
36
+
37
+ def apply(self, request: httpx.Request) -> httpx.Request:
38
+ # httpx requests are immutable; re-build with updated headers.
39
+ return httpx.Request(
40
+ method=request.method,
41
+ url=request.url,
42
+ headers={**dict(request.headers), "Authorization": f"Bearer {self._token}"},
43
+ content=request.content,
44
+ )
45
+
46
+
47
+ class BasicAuth:
48
+ def __init__(self, username: str, password: str) -> None:
49
+ self._auth = httpx.BasicAuth(username, password)
50
+
51
+ def prepare(self, client: httpx.Client) -> None:
52
+ client.auth = self._auth
53
+
54
+ def apply(self, request: httpx.Request) -> httpx.Request:
55
+ return request
56
+
57
+
58
+ class CookieAuth:
59
+ def __init__(self, cookies: dict[str, str]) -> None:
60
+ self._cookies = cookies
61
+
62
+ def prepare(self, client: httpx.Client) -> None:
63
+ client.cookies.update(self._cookies)
64
+
65
+ def apply(self, request: httpx.Request) -> httpx.Request:
66
+ return request
67
+
68
+
69
+ class CasFormLogin:
70
+ """CAS-style form-login: GET login page → extract hidden fields → POST credentials → capture cookies."""
71
+
72
+ def __init__(
73
+ self,
74
+ login_url: str,
75
+ username: str,
76
+ password: str,
77
+ username_field: str = "username",
78
+ password_field: str = "password",
79
+ ) -> None:
80
+ self._login_url = login_url
81
+ self._username = username
82
+ self._password = password
83
+ self._username_field = username_field
84
+ self._password_field = password_field
85
+
86
+ def prepare(self, client: httpx.Client) -> None:
87
+ resp = client.get(self._login_url)
88
+ resp.raise_for_status()
89
+ form_data = _extract_form_fields(resp.content)
90
+ form_data[self._username_field] = self._username
91
+ form_data[self._password_field] = self._password
92
+ login_resp = client.post(self._login_url, data=form_data, follow_redirects=True)
93
+ login_resp.raise_for_status()
94
+
95
+ def apply(self, request: httpx.Request) -> httpx.Request:
96
+ return request
97
+
98
+
99
+ def _extract_form_fields(html: bytes) -> dict[str, str]:
100
+ """Extract all hidden input fields from the first <form> in *html*."""
101
+ sel = Selector(text=html.decode("utf-8", errors="replace"))
102
+ fields: dict[str, str] = {}
103
+ for inp in sel.css("form input[type=hidden]"):
104
+ name = inp.attrib.get("name", "")
105
+ value = inp.attrib.get("value", "")
106
+ if name:
107
+ fields[name] = value
108
+ return fields