Metadata-Version: 2.4
Name: gl-browser-use-binary
Version: 0.1.8
Summary: A typed Python package for running browser automation tasks through `browser-use`
Author: GDP Labs
Requires-Python: <3.14,>=3.11
Description-Content-Type: text/markdown
Requires-Dist: browser-use<0.12.0,>=0.11.13
Requires-Dist: gllm-core-binary<0.5.0,>=0.4.57
Requires-Dist: json-repair<0.61.0,>=0.60.1
Requires-Dist: pydantic<3.0.0,>=2.11.7
Requires-Dist: requests<3.0.0,>=2.32.4
Requires-Dist: pydantic-settings<3.0.0,>=2.5.2
Requires-Dist: certifi>=2026.10.19
Provides-Extra: steel
Requires-Dist: steel-sdk<0.20.0,>=0.19.0; extra == "steel"
Provides-Extra: opensandbox
Requires-Dist: opensandbox<0.2.0,>=0.1.9; extra == "opensandbox"
Requires-Dist: playwright<2.0.0,>=1.40.0; extra == "opensandbox"
Provides-Extra: infrastructure
Requires-Dist: gl-browser-use-binary[opensandbox,steel]; extra == "infrastructure"
Provides-Extra: minio
Requires-Dist: minio<8.0.0,>=7.2.20; extra == "minio"
Provides-Extra: storage
Requires-Dist: gl-browser-use-binary[minio]; extra == "storage"
Provides-Extra: full
Requires-Dist: gl-browser-use-binary[infrastructure,storage]; extra == "full"

# GL Browser Use

GL Browser Use is a typed Python package for running browser automation tasks through `browser-use`. It provides a stable client facade, structured stream events, explicit result objects, optional Steel or OpenSandbox browser infrastructure, optional MinIO/S3-compatible recording storage, and bounded retries for recoverable browser-session failures.

## Installation

Use pip 26.2.2 or newer, or uv 0.13.4 or newer. For existing environments,
follow the [Python eMudhra/emSign security advisory](https://discuss.python.org/t/information-for-python-users-regarding-emudhra-emsign-ca-certificate-security-incident/109576)
to bootstrap an updated installer securely. This SDK requires certifi 2026.10.19
or newer; upgrading certifi does not update the certificate bundle embedded in
pip or uv. Rebuild browser container images with updated system CA certificates.
Upgrade sigstore to at least 4.5.1 if it is installed separately.

### Published binary package

Install the published binary distribution for normal application use:

```bash
pip install gl-browser-use-binary
```

Supported Python versions are 3.11, 3.12, and 3.13.

Install optional providers with the binary package only when you need them:

```bash
pip install "gl-browser-use-binary[steel]"           # Steel browser infrastructure
pip install "gl-browser-use-binary[opensandbox]"     # OpenSandbox browser infrastructure
pip install "gl-browser-use-binary[infrastructure]"  # All browser infrastructure providers
pip install "gl-browser-use-binary[minio]"           # MinIO/S3-compatible object storage
pip install "gl-browser-use-binary[storage]"         # All object storage providers
pip install "gl-browser-use-binary[full]"            # Infrastructure + storage providers
```

Use the concrete extras (`steel`, `opensandbox`, `minio`) in application dependency files when you want to pin exactly which provider you depend on. The slot extras (`infrastructure`, `storage`, `full`) are convenience aliases and may include more providers later.

The repository project is named `gl-browser-use`; the release workflow compiles and publishes it as `gl-browser-use-binary`. Both use the same `gl_browser_use` import namespace.

### Local development

From the repository checkout, install the source project and development dependencies with `uv`:

```bash
cd libs/gl-browser-use
uv sync --all-extras --group dev
```

## Quick Start

`BrowserUseClient` supports streaming and non-streaming execution. API keys can be passed directly or, when omitted, read from environment variables.

```python
import asyncio

from gl_browser_use import BrowserUseClient, BrowserUseClientConfig
from gl_browser_use.infrastructure import SteelBrowserInfrastructure
from gl_browser_use.storage import MinIOS3CompatibleStorage


async def main() -> None:
    client = BrowserUseClient(
        config=BrowserUseClientConfig(
            llm_openai_api_key="...",
            page_extraction_llm_openai_api_key="...",
            max_session_retries=2,
            session_retry_delay_in_s=3.0,
        ),
        infrastructure=SteelBrowserInfrastructure(),  # reads STEEL_API_KEY by default
        storage=MinIOS3CompatibleStorage.from_environment(),
    )

    async for event in client.run("Open Hacker News and list five article titles"):
        print(event.content)


asyncio.run(main())
```

For a single aggregated result:

```python
from gl_browser_use import BrowserUseClient, BrowserUseClientConfig
from gl_browser_use.infrastructure import SteelBrowserInfrastructure

client = BrowserUseClient(
    config=BrowserUseClientConfig(
        llm_openai_api_key="...",
        page_extraction_llm_openai_api_key="...",
    ),
    infrastructure=SteelBrowserInfrastructure(),
)

result = client.run_sync("Open Hacker News and list five article titles")

print(result.status)
print(result.final_output)
print(result.session_id)
print(result.streaming_url)
print(result.recording_url)
print(result.metadata)
```

## Configuration

`BrowserUseClientConfig` validates required values when the client is created. If `llm_openai_api_key` or `page_extraction_llm_openai_api_key` is omitted, it defaults to `OPENAI_API_KEY`. An explicit `None` is kept as missing, and is reported by validation.

Common client options:

1. `llm_openai_model`: primary browser-control model. Default: `o3`.
2. `page_extraction_llm_openai_model`: page extraction model. Default: `gpt-5-mini`.
3. `sensitive_data`: optional browser-use placeholder map. It accepts either a flat
    placeholder-to-value map or a domain-scoped map. Values are excluded from normal
    config serialization and redacted from `redacted_dict()` output.
4. `extend_system_message`: optional system prompt extension.
5. `vision_detail_level`: `auto`, `low`, or `high`. Default: `auto`.
6. `llm_timeout_in_s`: optional LLM timeout.
7. `step_timeout_in_s`: per-step browser timeout. Default: `180`.
8. `agent_kwargs`: additional `browser_use.Agent` constructor arguments. It cannot
    override client-managed arguments such as the task, LLMs, browser session, `sensitive_data`,
    or timeouts.
9. `enable_cloud_sync`: kept for compatibility, with no effect: browser-use 0.11 removed cloud sync. Default: `False`.
10. `logging_level`: `debug`, `info`, `warning`, `error`, or `result`. Default: `info`. Process-wide: it configures browser-use's shared loggers once, and only when the application hasn't configured logging (see below).
11. `max_session_retries`: recoverable session retries. Default: `2`.
12. `session_retry_delay_in_s`: delay between retries. Default: `3.0`.

### Sensitive data and Agent options

```python
config = BrowserUseClientConfig(
    llm_openai_api_key="...",
    page_extraction_llm_openai_api_key="...",
    sensitive_data={"username": "example-user", "password": "example-password"},
    agent_kwargs={"use_vision": False, "max_actions_per_step": 2},
)
```

Keep one configured client per credential scope. Browser-use also accepts domain-scoped
`sensitive_data` maps. Never put real values in logs, serialized configuration, or
`agent_kwargs`.

When supplying sensitive data, restrict browser navigation to the expected domains (for
example, through the infrastructure session's browser-use `BrowserProfile(allowed_domains=...)`)
to reduce prompt-injection exfiltration risk. `agent_kwargs` cannot override browser/session
ownership (`browser`, `browser_profile`, and `browser_session` are reserved); configure those
controls through the selected infrastructure instead.

Optional provider environment variables:

1. `STEEL_API_KEY`: used by `SteelBrowserInfrastructure()` when `api_key` is omitted.
2. `OPENSANDBOX_DOMAIN`: OpenSandbox control-plane host. Default: `localhost:8080`.
3. `OPENSANDBOX_API_KEY`: required for shared OpenSandbox clusters; optional for local insecure servers.
4. `OBJECT_STORAGE_URL`: MinIO/S3 endpoint, for example `localhost:9001` or `https://storage.example`.
5. `OBJECT_STORAGE_USERNAME`: object storage access key.
6. `OBJECT_STORAGE_PASSWORD`: object storage secret key.
7. `OBJECT_STORAGE_BUCKET_NAME`: target bucket name.
8. `OBJECT_STORAGE_DIRECTORY_PREFIX`: optional object key prefix.
9. `OBJECT_STORAGE_SECURE`: `true` for HTTPS when the endpoint has no scheme.

**An environment variable is read only for an argument you omit.** An explicit value, including `None`, is used as given. So in a service that runs browser tasks for several users, pass each user's own settings explicitly: `SteelBrowserInfrastructure(api_key=None)` raises `BrowserUseConfigurationError` instead of running on the host's `STEEL_API_KEY`. The same applies to the OpenSandbox and storage settings, and to the two OpenAI keys in `BrowserUseClientConfig`.

The client never writes environment variables. One setting is still process-wide: `logging_level` is applied through browser-use's `setup_logging`, which configures shared, process-level loggers, and only when the application hasn't configured logging (the root logger has no handlers). The first client to run sets the level for the whole process, and an application with its own logging setup keeps its own levels. Configure logging in the application when clients need different levels.

Copy `.env.example` to `.env` for local development, then load it with your application environment manager.

## Runtime API

The client exposes three run methods:

1. `run(task)`: async generator that yields `BrowserUseStreamEvent` values as work progresses.
2. `run_once(task)`: async method that returns one `BrowserUseRunResult` and includes emitted events in `result.events`.
3. `run_sync(task)`: blocking wrapper around `run_once()` using `asyncio.run()`.

Do not call `run_sync()` from an already running event loop. Use `await run_once()` or `async for event in run()` in async applications.

Cancelling the stream by breaking from `async for` or calling `await stream.aclose()` cancels the underlying run task. Browser sessions and infrastructure sessions are released in cleanup.

## Error Contract

Failures return safe, actionable messages and preserve the legacy `error` string for compatibility. Error results and terminal stream events also expose `error_info` with:

1. `code`: a stable, library-scoped machine-readable code;
2. `message`: a bounded summary that explains what failed without secrets or provider internals;
3. `next_action`: the next step for the caller;
4. `retryable`: whether a retry may be appropriate;
5. `reference_id`: a run-level ID shared by the result, events, and structured logs; and
6. `details`: JSON-safe public diagnostic fields (see [Diagnostics and privacy](#diagnostics-and-privacy)); and
7. `recovery`: how to react (see [Recovery](#recovery)).

Each error type has two stable codes. The public `gl_browser_use.<category>.<condition>` code is exposed as `error.public_code` and `error_info.code`. The `GL-BU-<NAME>` code is the shared `gllm_core.exceptions.BaseError` code, exposed as `error.code` and attached to log records by `gllm_core` logging (installed from the public `gllm-core-binary` wheel). `gl_browser_use.errors.registered_error_codes()` returns the same mapping at runtime.

| Public code (`error_info.code`) | `BaseError` code (`error.code`) | Error type | Meaning |
| --- | --- | --- | --- |
| `gl_browser_use.error` | `GL-BU-ERROR` | `BrowserUseSDKError` | Unclassified SDK failure. |
| `gl_browser_use.dependency.missing` | `GL-BU-DEPENDENCY-MISSING` | `BrowserUseMissingDependencyError` | An optional provider dependency is unavailable. |
| `gl_browser_use.config.invalid` | `GL-BU-CONFIG-INVALID` | `BrowserUseConfigurationError` | Configuration is missing or invalid. |
| `gl_browser_use.execution.failed` | `GL-BU-EXECUTION-FAILED` | `BrowserUseExecutionError` | Browser task execution failed. |
| `gl_browser_use.task.failed` | `GL-BU-TASK-FAILED` | `BrowserUseTaskFailedError` | The underlying agent reported a failed task. |
| `gl_browser_use.task.output_invalid` | `GL-BU-TASK-OUTPUT-INVALID` | `BrowserUseTaskOutputError` | Structured task output could not be parsed or validated. |
| `gl_browser_use.session.retry_exhausted` | `GL-BU-SESSION-RETRY-EXHAUSTED` | `BrowserUseRetryExhaustedError` | Recoverable session failures exhausted the retry budget. |
| `gl_browser_use.execution.timeout` | `GL-BU-TIMEOUT` | `BrowserUseTimeoutError` | A browser step or dependency call timed out. |
| `gl_browser_use.provider.auth_failed` | `GL-BU-PROVIDER-AUTH` | `BrowserUseProviderAuthError` | A model or browser provider rejected the credentials or permissions. |
| `gl_browser_use.provider.rate_limited` | `GL-BU-PROVIDER-RATE-LIMIT` | `BrowserUseRateLimitError` | A model or browser provider rate-limited the request. |
| `gl_browser_use.provider.unavailable` | `GL-BU-PROVIDER-UNAVAILABLE` | `BrowserUseProviderUnavailableError` | A model or browser provider could not be reached or failed. |

Codes are append-only. A code is never reused for a different failure or removed; a code that is no longer emitted is marked deprecated here for at least one minor release first. Every new concrete error type must define both codes, a `default_message`, and a `DEFAULT_NEXT_ACTION`; class definition fails on a missing, malformed, or duplicate code.

Diagnostic logs use the same `gl_browser_use.` prefix. Recording diagnostics include `gl_browser_use.recording.resolve_failed`, `gl_browser_use.recording.schedule_failed`, `gl_browser_use.recording.storage_unavailable`, `gl_browser_use.recording.download_failed`, `gl_browser_use.recording.background_failed`, `gl_browser_use.recording.background_unavailable`, `gl_browser_use.recording.navigation_failed`, and `gl_browser_use.recording.upload_failed`. Cleanup diagnostics include `gl_browser_use.cleanup.endpoint_unavailable`, `gl_browser_use.cleanup.recorder_failed`, `gl_browser_use.cleanup.eventbus_failed`, `gl_browser_use.cleanup.agent_failed`, `gl_browser_use.cleanup.browser_failed`, `gl_browser_use.cleanup.infrastructure_failed`, and `gl_browser_use.cleanup.sandbox_failed`.

`str(error)`, terminal event `content`, error-event `thinking_and_activity_info["data_value"]`, and the legacy result `error` combine `message` with `next_action`, so callers can act without reading logs first. Public exception strings and error messages are bounded, sanitized summaries; detailed causes remain in redacted structured diagnostics.

Reference IDs use the `glbu_<hex>` format and are distinct from error codes. Use the `reference_id` when searching centralized logs or contacting support. Configuration diagnostics are redacted recursively, including API keys, nested secret-shaped values, and URL credentials; arbitrary non-JSON values are replaced with a safe placeholder. A recoverable session retry restarts the task from the beginning, so external actions can be repeated; make retried tasks idempotent when possible. Recording is optional telemetry: a recording failure is reported in safe metadata and cannot change the primary task status.

### Recovery

`error_info.recovery` (`BrowserUseRecoveryInfo`) tells callers how to react:

| Field | Meaning |
| --- | --- |
| `retryable` | Whether retrying the same task can succeed. Non-retryable failures, such as invalid configuration or rejected credentials, are always `False`. |
| `retry_after_seconds` | Seconds to wait before retrying, when known: the provider's `retry-after` value for rate limits, or `session_retry_delay_in_s` for session retries. `None` when not retryable or unknown. |
| `attempt`, `max_attempts` | The SDK's own retry budget for the run. `max_attempts` is `max_session_retries + 1`. |
| `escalation` | When to stop retrying and who to contact, for example support with the `reference_id`. |

| Failure | `retryable` | Default wait |
| --- | --- | --- |
| `provider.rate_limited` | `True` | Provider `retry-after` or `try again in <n>s`, when reported. |
| `provider.unavailable`, `execution.timeout` | `True` | None |
| `session.retry_exhausted` | `True` | `session_retry_delay_in_s` |
| `provider.auth_failed`, `config.invalid`, `dependency.missing`, `task.*`, `execution.failed` | `False` | None |

Provider failures are classified from exception types and HTTP status codes first, then from provider error text. browser-use records most model-provider errors as failed steps instead of raising them, so after repeated failed steps the SDK classifies the last recorded step error. The SDK retries only recoverable browser-session failures itself; for other retryable failures, the caller decides whether to retry using `recovery`.

### Logging and reference lookup

The SDK logs through the standard `logging` module under the `gl_browser_use` logger and never removes handlers that the embedding application installed. Route these records to your centralized log store with your own handlers, for example the `gllm-core` JSON formatter.

Each run creates one `reference_id` that is shared by every attempt, retry event, terminal event, result, and log record of that run. Failure records carry these `extra` fields:

| Field | Meaning |
| --- | --- |
| `reference_id` | The run's `glbu_<hex>` reference ID. |
| `error_code` / `base_error_code` | The public code and the `GL-BU-*` code. |
| `error_message`, `next_action` | The safe public message and next step. |
| `stage`, `attempt`, `max_attempts` | Where the failure happened and which attempt it was. |
| `retryable`, `retry_after_seconds` | The recovery guidance. |
| `details`, `diagnostics` | Public and internal diagnostics, both redacted. |
| `failure_type`, `cause` | The raw failure type and a redacted, bounded cause. |

A terminal failure writes exactly one `ERROR` record for its `reference_id` (`Browser-use execution failed` or `Browser-use run failed`). Session retries write a `WARNING` record (`Browser-use session failed; retrying`) per retried attempt. To investigate a failure a user reports, search the log store for `reference_id = <id>` and level `ERROR`.

If a log handler or formatter fails, the SDK attempts one fallback record with only `reference_id` and `error_code`, and the run still returns its original result, error code, message, recovery data, and reference ID.

### Diagnostics and privacy

Error diagnostics follow a fixed schema, `gl_browser_use.errors.DIAGNOSTIC_FIELDS`:

| Visibility | Fields | Where they appear |
| --- | --- | --- |
| Public | `stage`, `attempt`, `max_attempts`, `step_count`, `streaming_url`, `recording_url`, `session_id`, `missing_dependency`, `package_hint`, `missing`, `errors` | `error.details`, `error_info.details`, logs |
| Internal | `url`, `last_error`, `web_socket_debugger_url`, `config`, `browser_context`, and any unlisted field | `error.debug_info["diagnostics"]` and logs only |

All diagnostic values are redacted recursively and made JSON-safe. Raw objects, including exceptions, become `<non-serializable>` instead of their representation.

Redaction (`redact_text` / `redact_value`) masks:

- configured API keys, `sensitive_data` values, and the task prompt when it is at least 12 characters;
- values under secret-shaped keys such as `api_key`, `token`, `password`, `authorization`, and `secret`;
- URL credentials, secret query parameters, inline `key=value` secrets, `Bearer` tokens, and `sk-`/`pk-`/`rk-` style API keys;
- email addresses.

It applies to error messages, `error_info`, error diagnostics, structured log fields, and step-event tool-call arguments and outputs. The run result keeps `task` and `final_output` unchanged because they are the caller's own input and output, and so does the final stream event's `final_output`. Activity events keep the streaming URL unchanged so it can be embedded.

## Result Contract

`BrowserUseRunResult` contains:

1. `status`: `success`, `error`, or `cancelled`.
2. `task`: normalized task string.
3. `final_output`: final text extracted from the underlying agent, or `Task completed` when no final text is available.
4. `session_id`: infrastructure session ID when an external browser session was used.
5. `streaming_url`: browser debug or streaming URL when available.
6. `recording_url`: expected recording URL when recording is configured.
7. `steps`: number of browser-use steps executed.
8. `error`: terminal error message for error results.
9. `events`: collected stream events for `run_once()`.
10. `metadata`: attempt, retry, and recording metadata.

Recording metadata uses these statuses:

1. `disabled`: infrastructure, storage, or browser context is not available.
2. `unsupported`: the selected infrastructure does not support recording.
3. `unavailable`: storage is configured but not available.
4. `scheduled`: a background recording upload has been scheduled.
5. `failed`: recording was eligible but could not be scheduled, resolved, or uploaded.
6. `unknown`: recording may have started, but the terminal error did not include enough context to determine the final state.

## Streaming Contract

`BrowserUseStreamEvent` contains:

1. `event_type`
2. `content`
3. `thinking_and_activity_info`
4. `is_final`
5. `tool_info`
6. `metadata`
7. `final_output`: set only on the final event of a successful run, to the same unredacted answer as `BrowserUseRunResult.final_output`. Every other event has `None`. Use it rather than reading the answer out of `tool_info`, whose tool-call data is redacted.

Important event content values:

1. `Receive streaming URL`: emitted when a browser debug or streaming URL is available.
2. `Receive recording URL`: emitted when a recording URL can be resolved.
3. `Task completed`: emitted for the final successful step.

Activity events encode iframe URLs in `thinking_and_activity_info["data_value"]` as a JSON string:

```json
{"type": "iframe", "message": "<url>"}
```

Step events include serialized tool calls in `tool_info["tool_calls"]`.

## Retries And Errors

The client retries only classified recoverable browser-session failures, such as browser closure or websocket disconnect messages. Before each retry, it emits a retry status event. Retries are bounded by `max_session_retries`, so total attempts are `max_session_retries + 1`. Each retry restarts the task from the beginning and may replay external side effects, so use idempotent tasks or set `max_session_retries=0` when replay is unsafe.

Every task failure returns `BrowserUseRunResult(status="error")` (or ends the stream with one terminal error event), including recoverable failures that exhaust all attempts. Those carry the `gl_browser_use.session.retry_exhausted` code, with the last failure in the message, on the result, the terminal event and the error log alike. The client doesn't raise `BrowserUseRetryExhaustedError` for them; the type remains the source of that code and its recovery guidance.

Error types:

All error types derive from `BrowserUseSDKError`, which derives from `gllm_core.exceptions.BaseError` and `RuntimeError`.

1. `BrowserUseConfigurationError`: missing or invalid runtime configuration. `BrowserUseClientConfig` raises it for every validation failure, including construction, `model_validate`, and attribute assignment, instead of a raw pydantic `ValidationError`. Rejected input values are never included.
2. `BrowserUseDependencyError`: abstract base for optional provider dependency problems.
3. `BrowserUseMissingDependencyError`: optional provider extra is not installed.
4. `BrowserUseExecutionError`: execution-time failure.
5. `BrowserUseTaskFailedError`: the agent reported completion without a successful result.
6. `BrowserUseTaskOutputError`: structured task output could not be parsed or validated.
7. `BrowserUseRetryExhaustedError`: recoverable session retries were exhausted. It is reported through `error_info`, not raised.
8. `BrowserUseTimeoutError`: a browser step or dependency call timed out.
9. `BrowserUseProviderAuthError`: a provider rejected the credentials or permissions.
10. `BrowserUseRateLimitError`: a provider rate-limited the request.
11. `BrowserUseProviderUnavailableError`: a provider could not be reached or failed.

Unexpected third-party errors are converted at one boundary, `gl_browser_use.errors.normalize_error()`, before they reach results, events, or logs.

## Optional Providers

Optional providers are lazy-loaded. Importing `gl_browser_use` does not require Steel, OpenSandbox, or MinIO to be installed.

```python
from gl_browser_use.infrastructure import OpenSandboxBrowserInfrastructure, SteelBrowserInfrastructure
from gl_browser_use.storage import MinIOS3CompatibleStorage
```

`SteelBrowserInfrastructure` creates Steel browser sessions and provides CDP/streaming URLs.

`OpenSandboxBrowserInfrastructure` provisions the repo-committed Chrome example image (`opensandbox/chrome:latest`, built from `examples/opensandbox/chrome`) through a local or shared OpenSandbox server, exposes noVNC inspection URLs, connects over proxied CDP, and uploads WebM session recordings to object storage with the same deferred `recording_url` semantics as Steel. Pass `storage` to `BrowserUseClient` when recording is enabled.

**DevTools note:** the Chrome image exposes CDP on `0.0.0.0:9222` (`--remote-debugging-address=0.0.0.0` plus a `socat` forward for Chromium M113+). Rebuild from `examples/opensandbox/chrome` after pulling changes; GL Browser Use discovers CDP via `sandbox.get_endpoint(9222)`. See `examples/opensandbox/chrome/README.md`.

OpenSandbox local setup:

```bash
DOCKER_HOST=unix://${HOME}/.docker/desktop/docker.sock \
  OPENSANDBOX_INSECURE_SERVER=YES opensandbox-server

cd libs/gl-browser-use/examples/opensandbox/chrome
docker build -t opensandbox/chrome:latest .
playwright install chromium
```

`MinIOS3CompatibleStorage` uploads session recordings to MinIO or an S3-compatible service and returns presigned URLs.

## Development

From `libs/gl-browser-use`:

```bash
make install-dev
make test-unit
make test-integration
make lint
make build-check
```

Useful targets:

1. `make test`: run all tests.
2. `make test-unit`: run unit tests with coverage.
3. `make test-integration`: run integration-marked contract tests.
4. `make lint`: run Ruff checks.
5. `make format`: run Ruff fixes and formatter.
6. `make pre-commit`: run pre-commit hooks.
7. `make build-check`: build the package and validate artifacts with Twine.
