|
| 1 | +# Headless Python SDK |
| 2 | + |
| 3 | +`lockintime-headless` is the zero-runtime-dependency Python client for the local |
| 4 | +[Headless](https://github.com/LockInTime/headless) browser host. It supports CPython |
| 5 | +3.11 through 3.14 on macOS and Linux. |
| 6 | + |
| 7 | +The SDK connects directly to Headless's private per-user Unix socket. It never opens |
| 8 | +a TCP port, evaluates arbitrary JavaScript, or receives a credential password. The |
| 9 | +typed API and runtime validators are generated from the repository's canonical SDK |
| 10 | +schema. SDK versions use semantic versioning independently of the wire protocol. |
| 11 | + |
| 12 | +## Install |
| 13 | + |
| 14 | +```sh |
| 15 | +python -m pip install lockintime-headless |
| 16 | +``` |
| 17 | + |
| 18 | +The `headless` CLI must already be installed and available on `PATH` when using |
| 19 | +`launch()`. `connect()` only attaches to an existing host and never shuts it down. |
| 20 | + |
| 21 | +## Synchronous client |
| 22 | + |
| 23 | +```python |
| 24 | +from headless_sdk import Untrusted, connect |
| 25 | + |
| 26 | +with connect() as client: |
| 27 | + client.session_create(name="research") |
| 28 | + session = client.session("research") |
| 29 | + page = session.visit(url="https://example.com") |
| 30 | + assert isinstance(page, Untrusted) |
| 31 | + print(page.value["title"]) |
| 32 | +``` |
| 33 | + |
| 34 | +The equivalent CLI flow is: |
| 35 | + |
| 36 | +```sh |
| 37 | +headless start --background |
| 38 | +headless session create research |
| 39 | +headless --session research visit https://example.com |
| 40 | +``` |
| 41 | + |
| 42 | +## Asynchronous client |
| 43 | + |
| 44 | +```python |
| 45 | +import asyncio |
| 46 | + |
| 47 | +from headless_sdk import aconnect |
| 48 | + |
| 49 | + |
| 50 | +async def main() -> None: |
| 51 | + async with await aconnect() as client: |
| 52 | + session = await client.open_session("research") |
| 53 | + page = await session.inspect(text=True) |
| 54 | + print(page.value["text"]) |
| 55 | + |
| 56 | + |
| 57 | +asyncio.run(main()) |
| 58 | +``` |
| 59 | + |
| 60 | +Pass an `asyncio.Event` as `cancel=` or cancel the calling task. A cancellation or |
| 61 | +timeout before any bytes are written is retry-safe. Once request bytes have been |
| 62 | +written, timeout, cancellation, transport, framing, or response-validation failures |
| 63 | +raise `OperationOutcomeUnknown`. Do not retry that operation until you inspect host |
| 64 | +state. |
| 65 | + |
| 66 | +## Supervised host ownership |
| 67 | + |
| 68 | +```python |
| 69 | +from headless_sdk import launch |
| 70 | + |
| 71 | +with launch(allow=["example.com"]) as host: |
| 72 | + print(host.client.host_status["pid"]) |
| 73 | +``` |
| 74 | + |
| 75 | +`launch()` runs `headless start --supervised`, keeps the owner pipe open, and grants |
| 76 | +ownership only after the startup response PID matches the connected host PID. |
| 77 | +Closing the wrapper terminates and reaps only that owned launcher. It cannot adopt |
| 78 | +or stop a concurrently started shared host. Omit `presentation` to preserve the |
| 79 | +platform default. On macOS, pass `presentation="background"` or |
| 80 | +`presentation="foreground"` for an explicit override. Custom executable paths must |
| 81 | +be absolute. |
| 82 | + |
| 83 | +## Authentication and untrusted data |
| 84 | + |
| 85 | +Page-derived results are wrapped in `Untrusted[T]`; validate them before using them |
| 86 | +in privileged operations. `AUTH_REQUIRED` becomes `AuthenticationRequiredError` and |
| 87 | +its details are also untrusted. Login accepts only a challenge plus credential alias, |
| 88 | +or interactive mode: |
| 89 | + |
| 90 | +```python |
| 91 | +from headless_sdk import AuthenticationRequiredError |
| 92 | + |
| 93 | +try: |
| 94 | + session.click(role="button", name="Continue") |
| 95 | +except AuthenticationRequiredError as error: |
| 96 | + aliases = error.details.value["accounts"] |
| 97 | + session.auth_login( |
| 98 | + challenge=error.details.value["challenge"], |
| 99 | + account=aliases[0]["alias"], |
| 100 | + ) |
| 101 | +``` |
| 102 | + |
| 103 | +There is intentionally no password parameter. Password enrollment remains a trusted |
| 104 | +CLI or native UI operation. Sensitive cookie and storage values still require both |
| 105 | +the command flag and the host's diagnostics environment gate. |
| 106 | + |
| 107 | +## Compatibility and security limits |
| 108 | + |
| 109 | +- The SDK requires the exact bundled wire protocol version. A mismatch fails before |
| 110 | + result decoding. |
| 111 | +- Capabilities are negotiated on connect. Missing commands raise |
| 112 | + `UnsupportedCapabilityError`; the SDK does not emulate them. |
| 113 | +- Socket paths must be direct children of `/tmp/headless-<uid>`. The directory and |
| 114 | + socket must be owned by the current user, non-symlinks, and private (`0700` and |
| 115 | + `0600`, respectively). |
| 116 | +- The transport accepts exactly one newline-terminated JSON response up to 1 MiB. |
| 117 | +- The same-user boundary prevents other OS users from connecting. It does not defend |
| 118 | + against a malicious process already running as your OS account. |
| 119 | +- `connect()` never owns the host. Stop shared hosts only through an explicit user |
| 120 | + action. |
| 121 | + |
| 122 | +Report security problems according to the repository |
| 123 | +[security policy](https://github.com/LockInTime/headless/security/policy). Do not put |
| 124 | +secrets, cookies, private artifacts, or credential material in reports. |
| 125 | + |
| 126 | +## Release controls |
| 127 | + |
| 128 | +Python packages publish only from a `python-v<package-version>` tag whose commit is |
| 129 | +already merged into `main`. The GitHub `pypi` environment allows only `python-v*` tags |
| 130 | +and requires independent reviewer approval with administrator bypass disabled. PyPI |
| 131 | +trusted publishing is bound to that environment; no long-lived package token is stored |
| 132 | +in the repository. |
| 133 | + |
| 134 | +## Development checks |
| 135 | + |
| 136 | +From `packages/headless-python`: |
| 137 | + |
| 138 | +```sh |
| 139 | +python -m pip install -e '.[dev]' |
| 140 | +python scripts/generate.py --check |
| 141 | +ruff check . |
| 142 | +ruff format --check . |
| 143 | +mypy |
| 144 | +pytest |
| 145 | +python -m build |
| 146 | +python scripts/verify_package.py |
| 147 | +``` |
| 148 | + |
| 149 | +Publishing uses PyPI trusted publishing from reviewed `python-v*` tags. The package |
| 150 | +has its own semantic version and is not released by Headless product `v*` tags. |
0 commit comments