Skip to content

Commit 4eb47cf

Browse files
committed
fix(sdk): omit unsupported launch presentation
1 parent fd81c78 commit 4eb47cf

5 files changed

Lines changed: 29 additions & 18 deletions

File tree

packages/headless-python/README.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,17 @@ state.
6868
```python
6969
from headless_sdk import launch
7070

71-
with launch(presentation="background", allow=["example.com"]) as host:
71+
with launch(allow=["example.com"]) as host:
7272
print(host.client.host_status["pid"])
7373
```
7474

75-
`launch()` runs `headless start --background --supervised`, keeps the owner pipe open,
76-
and grants ownership only after the startup response PID matches the connected host
77-
PID. Closing the wrapper terminates and reaps only that owned launcher. It cannot
78-
adopt or stop a concurrently started shared host. Use `presentation="foreground"`
79-
to request the foreground app behavior. Custom executable paths must be absolute.
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.
8082

8183
## Authentication and untrusted data
8284

packages/headless-python/src/headless_sdk/generated.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
PROTOCOL_SCHEMA_VERSION = 1
1414
MAXIMUM_MESSAGE_BYTES = 1048576
1515
MAXIMUM_COMMAND_TIMEOUT_SECONDS = 125.0
16-
PROTOCOL_SCHEMA_SHA256 = 'c199f18185cfa05b5c16c9140e48e1f588c61188b5f2ea6fa58eea2ddc57dcbf'
16+
PROTOCOL_SCHEMA_SHA256 = '882634187c7ef02ec4ed51fff0e747114eadeff308c10bb9b3274b3630fad11d'
1717
PROTOCOL_FIXTURES_SHA256 = '0b51ffaa2d3e3aaf0c32adcfeb02c180dcbe44face0d49e1c332b69f403ae062'
1818
RESPONSE_ADDITIONAL_PROPERTIES = True
1919
COMMAND_ERROR_CODES = ('ARTIFACT_ERROR', 'AUTH_ACCOUNT_NOT_FOUND', 'AUTH_CHALLENGE_CONSUMED', 'AUTH_CHALLENGE_EXPIRED', 'AUTH_CHALLENGE_NOT_FOUND', 'AUTH_FORM_CHANGED', 'AUTH_ORIGIN_CHANGED', 'AUTH_REQUIRED', 'CREDENTIAL_ALIAS_EXISTS', 'ELEMENT_NOT_FOUND', 'FLOW_FAILED', 'HOST_STOPPING', 'HOST_UNAVAILABLE', 'INTERNAL_ERROR', 'INVALID_CAPTURE_FORMAT', 'INVALID_COMMAND', 'INVALID_FLOW', 'INVALID_INPUT', 'INVALID_REQUEST', 'INVALID_SESSION', 'MISSING_PARAMETER', 'OPERATION_FAILED', 'PEER_DENIED', 'RECORDER_UNAVAILABLE', 'RECORDING_ACTIVE', 'RECORDING_FAILED', 'RECORDING_NOT_ACTIVE', 'REGION_NOT_FOUND', 'RESPONSE_TOO_LARGE', 'SENSITIVE_DIAGNOSTICS_DISABLED', 'SESSION_EXISTS', 'SESSION_NOT_FOUND', 'TIMEOUT', 'UNSAFE_NAVIGATION', 'UNSAFE_RESOURCE_TYPE', 'UNSUPPORTED_CAPABILITY', 'USER_PRESENCE_DENIED', 'USER_PRESENCE_UNAVAILABLE', 'VAULT_LOCKED', 'VAULT_OPERATION_FAILED', 'VAULT_RESPONSE_INVALID', 'VAULT_UNAVAILABLE')
@@ -1661,7 +1661,7 @@ class AuthenticationRequired(TypedDict, total=False):
16611661
LOCAL_LIFECYCLE: dict[str, Any] = {'connect': {'errors': ['HOST_UNAVAILABLE'],
16621662
'ownership': 'shared',
16631663
'transport': 'local-unix-socket'},
1664-
'launch': {'argv': ['start', '--background', '--supervised'],
1664+
'launch': {'argv': ['start', '--supervised'],
16651665
'command': 'start',
16661666
'errors': ['HOST_START_FAILED',
16671667
'NAVIGATION_ALLOWLIST_CONFLICT',

packages/headless-python/src/headless_sdk/lifecycle.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ def launch(
346346
*,
347347
executable: str | os.PathLike[str] | None = None,
348348
socket_path: str | None = None,
349-
presentation: LaunchPresentation = "background",
349+
presentation: LaunchPresentation | None = None,
350350
allow: Sequence[str] = (),
351351
environment: Mapping[str, str] | None = None,
352352
startup_timeout: float = _DEFAULT_STARTUP_TIMEOUT,
@@ -374,18 +374,24 @@ def launch(
374374
default_socket_path(selected_environment) if socket_path is None else socket_path
375375
)
376376
validate_socket_location(selected_socket)
377-
if not isinstance(presentation, str) or presentation not in LAUNCH_PRESENTATIONS:
377+
if presentation is not None and (
378+
not isinstance(presentation, str) or presentation not in LAUNCH_PRESENTATIONS
379+
):
378380
raise ValidationError(f"presentation must be one of {', '.join(LAUNCH_PRESENTATIONS)}")
379381
if isinstance(allow, (str, bytes)) or not isinstance(allow, Sequence):
380382
raise ValidationError("allow must be a sequence of host patterns")
381383
presentation_flags = {f"--{value}" for value in LAUNCH_PRESENTATIONS}
382384
argv = cast(list[str], list(LOCAL_LIFECYCLE["launch"]["argv"]))
383385
existing_flags = [argument for argument in argv if argument in presentation_flags]
384-
if len(existing_flags) != 1:
386+
if existing_flags:
385387
raise ValidationError("generated launch argv has an invalid presentation flag")
386-
argv = [
387-
f"--{presentation}" if argument in presentation_flags else argument for argument in argv
388+
supervised_indexes = [
389+
index for index, argument in enumerate(argv) if argument == "--supervised"
388390
]
391+
if len(supervised_indexes) != 1:
392+
raise ValidationError("generated launch argv has an invalid supervised flag")
393+
if presentation is not None:
394+
argv.insert(supervised_indexes[0], f"--{presentation}")
389395
allow_definition = next(
390396
(option for option in LOCAL_LIFECYCLE["launch"]["options"] if option["name"] == "allow"),
391397
None,
@@ -537,7 +543,7 @@ async def alaunch(
537543
*,
538544
executable: str | os.PathLike[str] | None = None,
539545
socket_path: str | None = None,
540-
presentation: LaunchPresentation = "background",
546+
presentation: LaunchPresentation | None = None,
541547
allow: Sequence[str] = (),
542548
environment: Mapping[str, str] | None = None,
543549
startup_timeout: float = _DEFAULT_STARTUP_TIMEOUT,

packages/headless-python/tests/helpers.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,9 +139,12 @@ def write_mock_launcher(directory: Path) -> Path:
139139
140140
PROTOCOL_VERSION = {PROTOCOL_VERSION!r}
141141
mode = os.environ.get("HEADLESS_TEST_MODE", "owned")
142-
expected_presentation = os.environ.get("HEADLESS_TEST_PRESENTATION", "background")
143-
expected = ["start", "--" + expected_presentation, "--supervised"]
144-
if sys.argv[1:4] != expected:
142+
expected_presentation = os.environ.get("HEADLESS_TEST_PRESENTATION")
143+
expected = ["start"]
144+
if expected_presentation:
145+
expected.append("--" + expected_presentation)
146+
expected.append("--supervised")
147+
if sys.argv[1:1 + len(expected)] != expected:
145148
sys.exit(64)
146149
socket_path = os.environ["HEADLESS_SOCKET"]
147150
commands = json.loads(os.environ["HEADLESS_TEST_COMMANDS"])

packages/headless-python/tests/test_lifecycle.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def test_supervised_launch_owns_only_exact_matching_host(tmp_path: Path) -> None
6767
}
6868
host = launch(executable=executable, socket_path=socket_path, environment=environment())
6969
try:
70-
assert LOCAL_LIFECYCLE["launch"]["argv"] == ["start", "--background", "--supervised"]
70+
assert LOCAL_LIFECYCLE["launch"]["argv"] == ["start", "--supervised"]
7171
assert host.pid == host.client.host_status["pid"]
7272
assert host.launcher_pid > 0
7373
assert signal.getsignal(signal.SIGINT) == signal_handlers[signal.SIGINT]

0 commit comments

Comments
 (0)