brinqz-agent 0.2.0__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.
Files changed (43) hide show
  1. brinqz_agent-0.2.0/MANIFEST.in +10 -0
  2. brinqz_agent-0.2.0/PKG-INFO +199 -0
  3. brinqz_agent-0.2.0/README.md +167 -0
  4. brinqz_agent-0.2.0/pyproject.toml +60 -0
  5. brinqz_agent-0.2.0/setup.cfg +4 -0
  6. brinqz_agent-0.2.0/src/brinqz_agent/__init__.py +6 -0
  7. brinqz_agent-0.2.0/src/brinqz_agent/__main__.py +4 -0
  8. brinqz_agent-0.2.0/src/brinqz_agent/artifacts.py +173 -0
  9. brinqz_agent-0.2.0/src/brinqz_agent/background.py +100 -0
  10. brinqz_agent-0.2.0/src/brinqz_agent/cli.py +560 -0
  11. brinqz_agent-0.2.0/src/brinqz_agent/cloud.py +910 -0
  12. brinqz_agent-0.2.0/src/brinqz_agent/compatibility.py +24 -0
  13. brinqz_agent-0.2.0/src/brinqz_agent/config.py +170 -0
  14. brinqz_agent-0.2.0/src/brinqz_agent/containment.py +621 -0
  15. brinqz_agent-0.2.0/src/brinqz_agent/execution.py +244 -0
  16. brinqz_agent-0.2.0/src/brinqz_agent/execution_lifecycle.py +456 -0
  17. brinqz_agent-0.2.0/src/brinqz_agent/execution_state.py +206 -0
  18. brinqz_agent-0.2.0/src/brinqz_agent/identity.py +59 -0
  19. brinqz_agent-0.2.0/src/brinqz_agent/lifecycle.py +265 -0
  20. brinqz_agent-0.2.0/src/brinqz_agent/listeners/__init__.py +1 -0
  21. brinqz_agent-0.2.0/src/brinqz_agent/listeners/brinqz_robot_listener.py +101 -0
  22. brinqz_agent-0.2.0/src/brinqz_agent/live_events.py +441 -0
  23. brinqz_agent-0.2.0/src/brinqz_agent/managed_runtime.py +833 -0
  24. brinqz_agent-0.2.0/src/brinqz_agent/pabot.py +34 -0
  25. brinqz_agent-0.2.0/src/brinqz_agent/packages.py +180 -0
  26. brinqz_agent-0.2.0/src/brinqz_agent/pairing.py +135 -0
  27. brinqz_agent-0.2.0/src/brinqz_agent/persistence.py +46 -0
  28. brinqz_agent-0.2.0/src/brinqz_agent/platforms.py +123 -0
  29. brinqz_agent-0.2.0/src/brinqz_agent/poll_health.py +83 -0
  30. brinqz_agent-0.2.0/src/brinqz_agent/preparation.py +284 -0
  31. brinqz_agent-0.2.0/src/brinqz_agent/robot.py +822 -0
  32. brinqz_agent-0.2.0/src/brinqz_agent/runtime.py +578 -0
  33. brinqz_agent-0.2.0/src/brinqz_agent/service.py +329 -0
  34. brinqz_agent-0.2.0/src/brinqz_agent/setup_ui.py +401 -0
  35. brinqz_agent-0.2.0/src/brinqz_agent/setup_workflow.py +658 -0
  36. brinqz_agent-0.2.0/src/brinqz_agent/startup.py +402 -0
  37. brinqz_agent-0.2.0/src/brinqz_agent/version.py +3 -0
  38. brinqz_agent-0.2.0/src/brinqz_agent.egg-info/PKG-INFO +199 -0
  39. brinqz_agent-0.2.0/src/brinqz_agent.egg-info/SOURCES.txt +41 -0
  40. brinqz_agent-0.2.0/src/brinqz_agent.egg-info/dependency_links.txt +1 -0
  41. brinqz_agent-0.2.0/src/brinqz_agent.egg-info/entry_points.txt +2 -0
  42. brinqz_agent-0.2.0/src/brinqz_agent.egg-info/requires.txt +5 -0
  43. brinqz_agent-0.2.0/src/brinqz_agent.egg-info/top_level.txt +1 -0
@@ -0,0 +1,10 @@
1
+ include README.md
2
+ include pyproject.toml
3
+ graft src
4
+ prune tests
5
+ prune docs
6
+ prune .github
7
+ prune scripts
8
+ global-exclude *.py[cod]
9
+ global-exclude __pycache__
10
+ global-exclude .DS_Store
@@ -0,0 +1,199 @@
1
+ Metadata-Version: 2.4
2
+ Name: brinqz-agent
3
+ Version: 0.2.0
4
+ Summary: Secure local execution agent for Brinqz Cloud
5
+ Author: Brinqz
6
+ Maintainer: Brinqz
7
+ License-Expression: LicenseRef-Proprietary
8
+ Project-URL: Homepage, https://brinqz.com
9
+ Project-URL: Documentation, https://github.com/ronnieparalejas/brinqz-agent#readme
10
+ Project-URL: Source, https://github.com/ronnieparalejas/brinqz-agent
11
+ Project-URL: Issues, https://github.com/ronnieparalejas/brinqz-agent/issues
12
+ Project-URL: Support, https://github.com/ronnieparalejas/brinqz-agent/issues
13
+ Project-URL: Brinqz Cloud, https://app.brinqz.com
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Environment :: Console
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Operating System :: MacOS
18
+ Classifier: Operating System :: Microsoft :: Windows
19
+ Classifier: Operating System :: POSIX :: Linux
20
+ Classifier: Programming Language :: Python :: 3 :: Only
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
25
+ Classifier: Topic :: Software Development :: Testing
26
+ Requires-Python: <4,>=3.11
27
+ Description-Content-Type: text/markdown
28
+ Provides-Extra: dev
29
+ Requires-Dist: build>=1.2.2; extra == "dev"
30
+ Requires-Dist: pytest>=8.3; extra == "dev"
31
+ Requires-Dist: twine>=6.1; extra == "dev"
32
+
33
+ # Brinqz Agent
34
+
35
+ Brinqz Agent is the local command-line agent that runs approved Robot Framework
36
+ test executions on your machine. Brinqz Cloud is its control plane: the Agent
37
+ pairs with Cloud, receives execution work, runs it through the configured local
38
+ test runtime, and returns approved results and artifacts.
39
+
40
+ Brinqz Agent is currently a **Public Preview / Alpha** release. It supports
41
+ Windows, macOS, and Linux and requires Python 3.11 or newer (Python 4 is not
42
+ supported).
43
+
44
+ ## Install and set up
45
+
46
+ Install or upgrade the Agent, then follow its guided setup:
47
+
48
+ ```console
49
+ python -m pip install --upgrade brinqz-agent
50
+ brinqz-agent setup
51
+ ```
52
+
53
+ For an isolated command-line installation, you can use
54
+ [pipx](https://pipx.pypa.io/):
55
+
56
+ ```console
57
+ pipx install brinqz-agent
58
+ brinqz-agent setup
59
+ ```
60
+
61
+ The default control-plane URL is `https://app.brinqz.com`. For local
62
+ development only, you can select a `.test` Cloud origin:
63
+
64
+ ```console
65
+ brinqz-agent setup --cloud-url http://app.brinqz.test
66
+ ```
67
+
68
+ Setup guides you through Cloud pairing and capability choices, prepares an
69
+ isolated Brinqz-managed execution runtime, registers per-user automatic startup,
70
+ starts the Agent, and verifies Cloud health. Robot Framework is always enabled.
71
+ Browser testing is recommended and defaults to Chromium; Firefox and WebKit are
72
+ optional. Parallel execution with Pabot is optional.
73
+
74
+ It is safe to rerun `brinqz-agent setup`. A healthy matching runtime is reused.
75
+ If repair or an upgrade is needed, setup builds and validates a replacement at
76
+ its final path before selecting it, and retains the previous working runtime
77
+ until activation succeeds.
78
+
79
+ ## Setup choices and managed deployment
80
+
81
+ Normal users only need the two commands above. Managed deployments and CI can
82
+ inspect advanced options with `brinqz-agent setup --help`. Common examples are:
83
+
84
+ ```console
85
+ # Recommended Chromium and Pabot capabilities
86
+ brinqz-agent setup --yes --recommended
87
+
88
+ # Explicit browsers and parallel execution
89
+ brinqz-agent setup --yes --browsers chromium,firefox --with-pabot
90
+
91
+ # Robot-only runtime without background registration
92
+ brinqz-agent setup --yes --robot-only --no-startup
93
+ ```
94
+
95
+ Use `--cloud-url` to select a different approved Cloud origin. Rerun setup with
96
+ `--browsers` or `--with-pabot` to change capabilities. Setup never installs
97
+ project credentials or project secrets.
98
+
99
+ ## Execution runtime
100
+
101
+ The Agent package environment and execution environment remain separate. Setup
102
+ creates a dedicated virtual environment under the user-scoped Agent data
103
+ directory and never installs Robot Framework, Browser Library, browsers, or
104
+ Pabot globally. It does not change the global `PATH` and does not require
105
+ administrator or root privileges.
106
+
107
+ Browser setup prefers the official BrowserBatteries distribution, which avoids
108
+ a separate Node.js installation on supported systems. On an unsupported system,
109
+ setup can use an already-installed supported Node.js LTS runtime; it never
110
+ silently installs Node.js or operating-system packages. If Linux browser system
111
+ dependencies are missing, setup reports the requirement without automatically
112
+ using `sudo`.
113
+
114
+ Use the following command to inspect the Agent configuration, selected runtime,
115
+ and required local capabilities:
116
+
117
+ ```console
118
+ brinqz-agent doctor
119
+ ```
120
+
121
+ Advanced users can keep a compatible external runtime with `brinqz-agent
122
+ runtime use <python-path>`. Setup preserves a valid manually selected runtime;
123
+ use `--managed-runtime` when deliberately switching back to Brinqz management.
124
+
125
+ ## Linux display behavior
126
+
127
+ Headless Robot and Browser setup works without `DISPLAY`. Headed browser
128
+ execution requires a real logged-in graphical session and a valid `DISPLAY` or
129
+ Wayland environment inherited by the Agent. The Agent does not fabricate display
130
+ variables. A headed request without a display fails with a concise diagnostic.
131
+ Some distributions require browser system libraries installed by an
132
+ administrator; setup explains those requirements and does not elevate itself.
133
+
134
+ ## Background lifecycle
135
+
136
+ The background registration is per-user: Task Scheduler on Windows, a
137
+ LaunchAgent on macOS, or a systemd user service on Linux.
138
+
139
+ ```console
140
+ brinqz-agent install
141
+ brinqz-agent status
142
+ brinqz-agent start
143
+ brinqz-agent stop
144
+ brinqz-agent restart
145
+ brinqz-agent uninstall
146
+ ```
147
+
148
+ `uninstall` removes background startup registration but preserves pairing,
149
+ managed runtimes, runtime selection, reports, and execution history. Remove the
150
+ user-scoped Brinqz Agent data directory separately only when you deliberately
151
+ want to delete those preserved files. `brinqz-agent run` is
152
+ available as a foreground diagnostic mode.
153
+
154
+ Managed data locations are:
155
+
156
+ - Windows: `%LOCALAPPDATA%\Brinqz Agent` (runtime under `runtime`)
157
+ - macOS: `~/Library/Application Support/Brinqz Agent`
158
+ - Linux: `${XDG_DATA_HOME:-~/.local/share}/brinqz-agent`
159
+
160
+ ## Security model
161
+
162
+ The Agent initiates outbound communication to Brinqz Cloud and does not open
163
+ inbound ports. Pairing establishes a local machine identity through Cloud's
164
+ browser-approval flow, and approved machine credentials are stored in the
165
+ user-scoped Agent configuration.
166
+
167
+ Project credentials are not entered or configured through Agent setup. An
168
+ execution that requires an unavailable local secret is refused before process
169
+ launch. The Agent executes prepared Robot Framework packages through a fixed
170
+ Robot or Pabot invocation; it does not provide arbitrary remote shell
171
+ execution.
172
+
173
+ Downloaded packages are authenticated, size and checksum verified, and safely
174
+ extracted. Execution runs in a contained process boundary, and only approved
175
+ report artifacts are returned to Cloud.
176
+
177
+ ## Public Preview limitations
178
+
179
+ This Alpha release is command-line only. It does not install Python, Node.js,
180
+ Linux operating-system browser dependencies, or project secrets. It does not
181
+ include a GUI or tray application, a native installer, self-update, or
182
+ Runner-local project-secret storage. Interfaces and operational guidance may
183
+ change before a stable release.
184
+
185
+ ## Support
186
+
187
+ Report problems and request support through the
188
+ [Brinqz Agent issue tracker](https://github.com/ronnieparalejas/brinqz-agent/issues).
189
+
190
+ ## Development
191
+
192
+ ```console
193
+ python -m venv .venv
194
+ # Activate .venv using your shell, then:
195
+ python -m pip install -e ".[dev]"
196
+ python -m pytest
197
+ ```
198
+
199
+ The repository contains additional manual validation plans for maintainers.
@@ -0,0 +1,167 @@
1
+ # Brinqz Agent
2
+
3
+ Brinqz Agent is the local command-line agent that runs approved Robot Framework
4
+ test executions on your machine. Brinqz Cloud is its control plane: the Agent
5
+ pairs with Cloud, receives execution work, runs it through the configured local
6
+ test runtime, and returns approved results and artifacts.
7
+
8
+ Brinqz Agent is currently a **Public Preview / Alpha** release. It supports
9
+ Windows, macOS, and Linux and requires Python 3.11 or newer (Python 4 is not
10
+ supported).
11
+
12
+ ## Install and set up
13
+
14
+ Install or upgrade the Agent, then follow its guided setup:
15
+
16
+ ```console
17
+ python -m pip install --upgrade brinqz-agent
18
+ brinqz-agent setup
19
+ ```
20
+
21
+ For an isolated command-line installation, you can use
22
+ [pipx](https://pipx.pypa.io/):
23
+
24
+ ```console
25
+ pipx install brinqz-agent
26
+ brinqz-agent setup
27
+ ```
28
+
29
+ The default control-plane URL is `https://app.brinqz.com`. For local
30
+ development only, you can select a `.test` Cloud origin:
31
+
32
+ ```console
33
+ brinqz-agent setup --cloud-url http://app.brinqz.test
34
+ ```
35
+
36
+ Setup guides you through Cloud pairing and capability choices, prepares an
37
+ isolated Brinqz-managed execution runtime, registers per-user automatic startup,
38
+ starts the Agent, and verifies Cloud health. Robot Framework is always enabled.
39
+ Browser testing is recommended and defaults to Chromium; Firefox and WebKit are
40
+ optional. Parallel execution with Pabot is optional.
41
+
42
+ It is safe to rerun `brinqz-agent setup`. A healthy matching runtime is reused.
43
+ If repair or an upgrade is needed, setup builds and validates a replacement at
44
+ its final path before selecting it, and retains the previous working runtime
45
+ until activation succeeds.
46
+
47
+ ## Setup choices and managed deployment
48
+
49
+ Normal users only need the two commands above. Managed deployments and CI can
50
+ inspect advanced options with `brinqz-agent setup --help`. Common examples are:
51
+
52
+ ```console
53
+ # Recommended Chromium and Pabot capabilities
54
+ brinqz-agent setup --yes --recommended
55
+
56
+ # Explicit browsers and parallel execution
57
+ brinqz-agent setup --yes --browsers chromium,firefox --with-pabot
58
+
59
+ # Robot-only runtime without background registration
60
+ brinqz-agent setup --yes --robot-only --no-startup
61
+ ```
62
+
63
+ Use `--cloud-url` to select a different approved Cloud origin. Rerun setup with
64
+ `--browsers` or `--with-pabot` to change capabilities. Setup never installs
65
+ project credentials or project secrets.
66
+
67
+ ## Execution runtime
68
+
69
+ The Agent package environment and execution environment remain separate. Setup
70
+ creates a dedicated virtual environment under the user-scoped Agent data
71
+ directory and never installs Robot Framework, Browser Library, browsers, or
72
+ Pabot globally. It does not change the global `PATH` and does not require
73
+ administrator or root privileges.
74
+
75
+ Browser setup prefers the official BrowserBatteries distribution, which avoids
76
+ a separate Node.js installation on supported systems. On an unsupported system,
77
+ setup can use an already-installed supported Node.js LTS runtime; it never
78
+ silently installs Node.js or operating-system packages. If Linux browser system
79
+ dependencies are missing, setup reports the requirement without automatically
80
+ using `sudo`.
81
+
82
+ Use the following command to inspect the Agent configuration, selected runtime,
83
+ and required local capabilities:
84
+
85
+ ```console
86
+ brinqz-agent doctor
87
+ ```
88
+
89
+ Advanced users can keep a compatible external runtime with `brinqz-agent
90
+ runtime use <python-path>`. Setup preserves a valid manually selected runtime;
91
+ use `--managed-runtime` when deliberately switching back to Brinqz management.
92
+
93
+ ## Linux display behavior
94
+
95
+ Headless Robot and Browser setup works without `DISPLAY`. Headed browser
96
+ execution requires a real logged-in graphical session and a valid `DISPLAY` or
97
+ Wayland environment inherited by the Agent. The Agent does not fabricate display
98
+ variables. A headed request without a display fails with a concise diagnostic.
99
+ Some distributions require browser system libraries installed by an
100
+ administrator; setup explains those requirements and does not elevate itself.
101
+
102
+ ## Background lifecycle
103
+
104
+ The background registration is per-user: Task Scheduler on Windows, a
105
+ LaunchAgent on macOS, or a systemd user service on Linux.
106
+
107
+ ```console
108
+ brinqz-agent install
109
+ brinqz-agent status
110
+ brinqz-agent start
111
+ brinqz-agent stop
112
+ brinqz-agent restart
113
+ brinqz-agent uninstall
114
+ ```
115
+
116
+ `uninstall` removes background startup registration but preserves pairing,
117
+ managed runtimes, runtime selection, reports, and execution history. Remove the
118
+ user-scoped Brinqz Agent data directory separately only when you deliberately
119
+ want to delete those preserved files. `brinqz-agent run` is
120
+ available as a foreground diagnostic mode.
121
+
122
+ Managed data locations are:
123
+
124
+ - Windows: `%LOCALAPPDATA%\Brinqz Agent` (runtime under `runtime`)
125
+ - macOS: `~/Library/Application Support/Brinqz Agent`
126
+ - Linux: `${XDG_DATA_HOME:-~/.local/share}/brinqz-agent`
127
+
128
+ ## Security model
129
+
130
+ The Agent initiates outbound communication to Brinqz Cloud and does not open
131
+ inbound ports. Pairing establishes a local machine identity through Cloud's
132
+ browser-approval flow, and approved machine credentials are stored in the
133
+ user-scoped Agent configuration.
134
+
135
+ Project credentials are not entered or configured through Agent setup. An
136
+ execution that requires an unavailable local secret is refused before process
137
+ launch. The Agent executes prepared Robot Framework packages through a fixed
138
+ Robot or Pabot invocation; it does not provide arbitrary remote shell
139
+ execution.
140
+
141
+ Downloaded packages are authenticated, size and checksum verified, and safely
142
+ extracted. Execution runs in a contained process boundary, and only approved
143
+ report artifacts are returned to Cloud.
144
+
145
+ ## Public Preview limitations
146
+
147
+ This Alpha release is command-line only. It does not install Python, Node.js,
148
+ Linux operating-system browser dependencies, or project secrets. It does not
149
+ include a GUI or tray application, a native installer, self-update, or
150
+ Runner-local project-secret storage. Interfaces and operational guidance may
151
+ change before a stable release.
152
+
153
+ ## Support
154
+
155
+ Report problems and request support through the
156
+ [Brinqz Agent issue tracker](https://github.com/ronnieparalejas/brinqz-agent/issues).
157
+
158
+ ## Development
159
+
160
+ ```console
161
+ python -m venv .venv
162
+ # Activate .venv using your shell, then:
163
+ python -m pip install -e ".[dev]"
164
+ python -m pytest
165
+ ```
166
+
167
+ The repository contains additional manual validation plans for maintainers.
@@ -0,0 +1,60 @@
1
+ [build-system]
2
+ requires = ["setuptools>=75"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "brinqz-agent"
7
+ dynamic = ["version"]
8
+ description = "Secure local execution agent for Brinqz Cloud"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11,<4"
11
+ license = "LicenseRef-Proprietary"
12
+ authors = [{ name = "Brinqz" }]
13
+ maintainers = [{ name = "Brinqz" }]
14
+ dependencies = []
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Environment :: Console",
18
+ "Intended Audience :: Developers",
19
+ "Operating System :: MacOS",
20
+ "Operating System :: Microsoft :: Windows",
21
+ "Operating System :: POSIX :: Linux",
22
+ "Programming Language :: Python :: 3 :: Only",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Programming Language :: Python :: 3.14",
27
+ "Topic :: Software Development :: Testing",
28
+ ]
29
+
30
+ [project.urls]
31
+ Homepage = "https://brinqz.com"
32
+ Documentation = "https://github.com/ronnieparalejas/brinqz-agent#readme"
33
+ Source = "https://github.com/ronnieparalejas/brinqz-agent"
34
+ Issues = "https://github.com/ronnieparalejas/brinqz-agent/issues"
35
+ Support = "https://github.com/ronnieparalejas/brinqz-agent/issues"
36
+ "Brinqz Cloud" = "https://app.brinqz.com"
37
+
38
+ [project.optional-dependencies]
39
+ dev = [
40
+ "build>=1.2.2",
41
+ "pytest>=8.3",
42
+ "twine>=6.1",
43
+ ]
44
+
45
+ [project.scripts]
46
+ brinqz-agent = "brinqz_agent.cli:main"
47
+
48
+ [tool.setuptools.packages.find]
49
+ where = ["src"]
50
+
51
+ [tool.setuptools.dynamic]
52
+ version = { attr = "brinqz_agent.version.__version__" }
53
+
54
+ [tool.setuptools.package-data]
55
+ brinqz_agent = ["listeners/brinqz_robot_listener.py"]
56
+
57
+ [tool.pytest.ini_options]
58
+ addopts = "-ra"
59
+ testpaths = ["tests"]
60
+ pythonpath = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ """Brinqz Agent CLI foundation."""
2
+
3
+ from brinqz_agent.version import __version__
4
+
5
+ __all__ = ["__version__"]
6
+
@@ -0,0 +1,4 @@
1
+ from brinqz_agent.cli import main
2
+
3
+ raise SystemExit(main())
4
+
@@ -0,0 +1,173 @@
1
+ """Allow-listed Robot result collection for the existing Cloud contract."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from xml.etree import ElementTree
7
+ import hashlib
8
+ import json
9
+ import os
10
+ import re
11
+
12
+ from brinqz_agent.cloud import ArtifactUpload
13
+
14
+ MAX_SCREENSHOTS = 25
15
+ MAX_SCREENSHOT_BYTES = 10 * 1024 * 1024
16
+ MAX_OUTPUT_XML_BYTES = 16 * 1024 * 1024
17
+ MAX_REPORT_SUMMARY_BYTES = 2 * 1024 * 1024
18
+
19
+
20
+ class ArtifactCollectionError(RuntimeError):
21
+ pass
22
+
23
+
24
+ def collect_cloud_artifacts(output_directory: Path) -> tuple[ArtifactUpload, ...]:
25
+ if not output_directory.is_dir() or output_directory.is_symlink():
26
+ return ()
27
+ root = output_directory.resolve()
28
+ artifacts = list(_screenshots(root))
29
+ output_xml = root / "output.xml"
30
+ if _safe_regular_file(output_xml, root):
31
+ size = output_xml.stat().st_size
32
+ if 0 < size <= MAX_OUTPUT_XML_BYTES:
33
+ sanitized = _sanitized_output_xml(output_xml)
34
+ artifacts.append(
35
+ _artifact(
36
+ sanitized,
37
+ "artifact-robot_output-output-xml",
38
+ "robot_output",
39
+ "output.xml",
40
+ "application/xml",
41
+ )
42
+ )
43
+ return tuple(artifacts)
44
+
45
+
46
+ def report_summary_path(output_directory: Path) -> Path | None:
47
+ if not output_directory.is_dir() or output_directory.is_symlink():
48
+ return None
49
+ root = output_directory.resolve()
50
+ candidate = root / "report-summary.json"
51
+ if not _safe_regular_file(candidate, root):
52
+ return None
53
+ size = candidate.stat().st_size
54
+ if size <= 0 or size > MAX_REPORT_SUMMARY_BYTES:
55
+ return None
56
+ try:
57
+ payload = json.loads(candidate.read_text(encoding="utf-8"))
58
+ except (OSError, UnicodeError, json.JSONDecodeError):
59
+ return None
60
+ return candidate if isinstance(payload, dict) else None
61
+
62
+
63
+ def _screenshots(root: Path):
64
+ screenshot_root = root / "browser" / "screenshot"
65
+ if not screenshot_root.is_dir() or screenshot_root.is_symlink():
66
+ return ()
67
+ found: list[ArtifactUpload] = []
68
+ for candidate in sorted(screenshot_root.rglob("*")):
69
+ if len(found) >= MAX_SCREENSHOTS:
70
+ break
71
+ if not _safe_regular_file(candidate, root):
72
+ continue
73
+ size = candidate.stat().st_size
74
+ content_type = _screenshot_type(candidate)
75
+ if not content_type or size <= 0 or size > MAX_SCREENSHOT_BYTES:
76
+ continue
77
+ if not _screenshot_magic(candidate, content_type):
78
+ continue
79
+ relative = candidate.resolve().relative_to(root).as_posix()
80
+ if not relative.startswith("browser/screenshot/"):
81
+ continue
82
+ found.append(
83
+ _artifact(
84
+ candidate,
85
+ _stable_artifact_id(relative, "screenshot"),
86
+ "screenshot",
87
+ relative,
88
+ content_type,
89
+ )
90
+ )
91
+ return tuple(found)
92
+
93
+
94
+ def _safe_regular_file(path: Path, root: Path) -> bool:
95
+ try:
96
+ if not path.is_file() or path.is_symlink():
97
+ return False
98
+ resolved = path.resolve()
99
+ return os.path.commonpath((str(root), str(resolved))) == str(root)
100
+ except (OSError, ValueError):
101
+ return False
102
+
103
+
104
+ def _sanitized_output_xml(source: Path) -> Path:
105
+ try:
106
+ content = source.read_text(encoding="utf-8")
107
+ if re.search(r"(?i)<!DOCTYPE|<!ENTITY", content):
108
+ raise ArtifactCollectionError("Robot output.xml contains a forbidden declaration.")
109
+ root = ElementTree.fromstring(content)
110
+ except (OSError, UnicodeError, ElementTree.ParseError) as error:
111
+ raise ArtifactCollectionError("Robot output.xml is invalid.") from error
112
+ if root.tag != "robot":
113
+ raise ArtifactCollectionError("Robot output.xml has an unexpected root element.")
114
+ sanitized = re.sub(
115
+ r"(?i)\b(password|token|secret|api[_-]?key|login_password)\b(\s*[:=]\s*)([^\s<]+)",
116
+ r"\1\2[redacted]",
117
+ content,
118
+ )
119
+ destination = source.with_name(".brinqz-cloud-output.xml")
120
+ destination.write_text(sanitized, encoding="utf-8")
121
+ return destination
122
+
123
+
124
+ def _artifact(
125
+ path: Path,
126
+ artifact_id: str,
127
+ artifact_type: str,
128
+ relative_path: str,
129
+ content_type: str,
130
+ ) -> ArtifactUpload:
131
+ size = path.stat().st_size
132
+ digest = hashlib.sha256()
133
+ with path.open("rb") as stream:
134
+ while chunk := stream.read(128 * 1024):
135
+ digest.update(chunk)
136
+ return ArtifactUpload(
137
+ artifact_id,
138
+ artifact_type,
139
+ relative_path,
140
+ content_type,
141
+ size,
142
+ digest.hexdigest(),
143
+ path,
144
+ )
145
+
146
+
147
+ def _screenshot_type(path: Path) -> str | None:
148
+ return {
149
+ ".png": "image/png",
150
+ ".jpg": "image/jpeg",
151
+ ".jpeg": "image/jpeg",
152
+ ".webp": "image/webp",
153
+ }.get(path.suffix.lower())
154
+
155
+
156
+ def _screenshot_magic(path: Path, content_type: str) -> bool:
157
+ with path.open("rb") as stream:
158
+ header = stream.read(12)
159
+ if content_type == "image/png":
160
+ return header.startswith(b"\x89PNG\r\n\x1a\n")
161
+ if content_type == "image/jpeg":
162
+ return header.startswith(b"\xff\xd8\xff")
163
+ return len(header) >= 12 and header[:4] == b"RIFF" and header[8:12] == b"WEBP"
164
+
165
+
166
+ def _stable_artifact_id(relative_path: str, kind: str) -> str:
167
+ value = f"artifact-{kind}-"
168
+ for character in relative_path:
169
+ if character.isascii() and character.isalnum():
170
+ value += character.lower()
171
+ elif character in "/.-_" and not value.endswith("-"):
172
+ value += "-"
173
+ return value.rstrip("-")