headroom-kit 0.1.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.
@@ -0,0 +1 @@
1
+ """Launch existing coding agents through a local Headroom proxy."""
@@ -0,0 +1,5 @@
1
+ """Support python -m headroom_kit."""
2
+
3
+ from headroom_kit.cli import main
4
+
5
+ raise SystemExit(main())
headroom_kit/_owner.py ADDED
@@ -0,0 +1,14 @@
1
+ """Private detached owner. Not a console script."""
2
+
3
+ import signal
4
+ import sys
5
+
6
+ from headroom_kit.proxy import owner_main
7
+ from headroom_kit.runtime import stop
8
+
9
+ if __name__ == "__main__":
10
+ # The owner is a new session. Without these, SIGTERM/SIGHUP skip cleanup
11
+ # and the proxy child keeps the port and control socket.
12
+ for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGHUP):
13
+ signal.signal(sig, stop)
14
+ sys.exit(owner_main())
headroom_kit/_serve.py ADDED
@@ -0,0 +1,8 @@
1
+ """Private proxy server. Not a console script."""
2
+
3
+ import sys
4
+
5
+ from headroom_kit.proxy import serve_main
6
+
7
+ if __name__ == "__main__":
8
+ sys.exit(serve_main())
headroom_kit/cli.py ADDED
@@ -0,0 +1,190 @@
1
+ """Public command surface for the installed Headroom Kit package."""
2
+
3
+ import importlib.metadata
4
+ import signal
5
+ import subprocess
6
+ import sys
7
+ from collections.abc import Sequence
8
+
9
+ from headroom_kit.copilot import run_copilot_auth
10
+ from headroom_kit.proxy import management
11
+ from headroom_kit.runtime import (
12
+ KitError,
13
+ executable,
14
+ installed_headroom_version,
15
+ load_config,
16
+ merge_config,
17
+ read_config,
18
+ run_agent,
19
+ say,
20
+ stop,
21
+ validate,
22
+ )
23
+ from headroom_kit.session import preflight, session
24
+
25
+ AGENTS = {
26
+ "codex": "codex-headroom",
27
+ "codex-app": "codex-app-headroom",
28
+ "copilot": "copilot-headroom",
29
+ "copilot-vscode": "copilot-vscode-headroom",
30
+ "pi": "pi-headroom",
31
+ "opencode": "opencode-headroom",
32
+ }
33
+ CLI_AGENTS = ("codex-headroom", "copilot-headroom", "pi-headroom", "opencode-headroom")
34
+ GUI_AGENTS = ("codex-app-headroom", "copilot-vscode-headroom")
35
+
36
+
37
+ def main(argv: Sequence[str] | None = None) -> int:
38
+ for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGHUP):
39
+ signal.signal(sig, stop)
40
+ try:
41
+ return dispatch(list(sys.argv[1:] if argv is None else argv))
42
+ except KitError as error:
43
+ say(str(error))
44
+ except (OSError, subprocess.SubprocessError):
45
+ say("A local process or file operation failed. Check paths, permissions, and disk space.")
46
+ return 1
47
+
48
+
49
+ def dispatch(argv: list[str]) -> int:
50
+ if not argv or argv == ["--help"] or argv == ["-h"]:
51
+ print_help()
52
+ return 0 if argv else 2
53
+ if argv == ["--version"]:
54
+ return print_version()
55
+ config, rest = take_config(argv)
56
+ if not rest:
57
+ print_help()
58
+ return 2
59
+ command, *args = rest
60
+ if command == "copilot-auth":
61
+ return run_copilot_auth(args)
62
+ if command == "status":
63
+ return status(args)
64
+ if command == "stop":
65
+ return management(["stop", *args])
66
+ if command == "run":
67
+ return run(config, args)
68
+ if command in ("--help", "-h"):
69
+ print_help()
70
+ return 0
71
+ raise KitError(f"Unknown command {command!r}. Run headroom-kit --help.")
72
+
73
+
74
+ def take_config(argv: list[str]) -> tuple[str | None, list[str]]:
75
+ if not argv or argv[0] != "--config":
76
+ return None, argv
77
+ if len(argv) < 2 or not argv[1] or argv[1].startswith("-"):
78
+ raise KitError("--config requires a path to a JSON object.")
79
+ return argv[1], argv[2:]
80
+
81
+
82
+ def status(args: list[str]) -> int:
83
+ if args:
84
+ raise KitError("status does not take arguments. Run headroom-kit status.")
85
+ return management(["status"])
86
+
87
+
88
+ def run(config: str | None, args: list[str]) -> int:
89
+ if not args or args[0] in ("--help", "-h"):
90
+ print_run_help()
91
+ return 0
92
+ agent, *rest = args
93
+ command = AGENTS.get(agent)
94
+ if command is None:
95
+ raise KitError(
96
+ f"Unknown agent {agent!r}. Choose {', '.join(AGENTS)}. Put agent arguments after --."
97
+ )
98
+ agent_args = agent_arguments(rest)
99
+ if forwards_help(command, agent_args):
100
+ return forward_agent(config, command, agent_args)
101
+ cfg = load_config(config)
102
+ validate(cfg)
103
+ return session(cfg, command, agent_args, installed_headroom_version())
104
+
105
+
106
+ def agent_arguments(args: list[str]) -> list[str]:
107
+ if not args or args[0] != "--":
108
+ raise KitError("Put agent arguments after --. Example: headroom-kit run codex --")
109
+ return args[1:]
110
+
111
+
112
+ def forwards_help(command: str, args: list[str]) -> bool:
113
+ if command in GUI_AGENTS and args in (["--help"], ["-h"]):
114
+ return True
115
+ options = args[: args.index("--")] if "--" in args else args
116
+ return command in CLI_AGENTS and any(is_help_option(command, arg) for arg in options)
117
+
118
+
119
+ def is_help_option(command: str, arg: str) -> bool:
120
+ return arg in ("--help", "-h", "--version", "-V") or (
121
+ arg == "-v" and command in ("pi-headroom", "opencode-headroom")
122
+ )
123
+
124
+
125
+ def forward_agent(config: str | None, command: str, args: list[str]) -> int:
126
+ if command in GUI_AGENTS:
127
+ print_gui_help(command)
128
+ return 0
129
+ cfg = merge_config(read_config(config))
130
+ if command == "opencode-headroom":
131
+ binary = executable(cfg["opencodeExecutable"], "HEADROOM_OPENCODE_EXECUTABLE")
132
+ return run_agent([binary, *args])
133
+ binary = preflight(cfg, command, args)
134
+ if not isinstance(binary, str):
135
+ raise KitError("The agent executable could not be resolved.")
136
+ return run_agent([binary, *args])
137
+
138
+
139
+ def print_version() -> int:
140
+ print(f"headroom-kit {distribution_version('headroom-kit')}")
141
+ print(f"headroom-ai {distribution_version('headroom-ai')}")
142
+ return 0
143
+
144
+
145
+ def distribution_version(name: str) -> str:
146
+ try:
147
+ return importlib.metadata.version(name)
148
+ except importlib.metadata.PackageNotFoundError:
149
+ return "not installed"
150
+
151
+
152
+ def print_help() -> None:
153
+ print(
154
+ """Usage:
155
+ headroom-kit [--config PATH] run <agent> -- <agent arguments...>
156
+ headroom-kit copilot-auth login
157
+ headroom-kit copilot-auth status
158
+ headroom-kit status
159
+ headroom-kit stop <port>
160
+ headroom-kit --help
161
+ headroom-kit --version
162
+
163
+ Agents: codex, codex-app, copilot, copilot-vscode, pi, opencode.
164
+
165
+ --config is a JSON object of non-secret integration defaults. HEADROOM_*
166
+ environment variables override the file, which overrides built-in defaults.
167
+ HEADROOM_VERSION and runtime-selection settings are rejected.
168
+
169
+ copilot-auth runs Headroom's Copilot login in this install. It does not
170
+ replace the headroom command and does not need a separate Headroom install.
171
+ status and stop do not need provider credentials or model downloads."""
172
+ )
173
+
174
+
175
+ def print_run_help() -> None:
176
+ print("Usage: headroom-kit [--config PATH] run <agent> -- <agent arguments...>")
177
+ print("Agents: " + ", ".join(AGENTS))
178
+ print("Agent help skips the proxy: headroom-kit run codex -- --help")
179
+
180
+
181
+ def print_gui_help(command: str) -> None:
182
+ if command == "copilot-vscode-headroom":
183
+ print("Usage: headroom-kit run copilot-vscode -- [path]")
184
+ else:
185
+ print("Usage: headroom-kit run codex-app --")
186
+ print("Launch with a shared local Headroom proxy. Use headroom-kit status/stop to manage it.")
187
+ print(
188
+ "Normal Codex and VS Code launches keep their existing settings. "
189
+ "Configure through HEADROOM_*."
190
+ )
@@ -0,0 +1,156 @@
1
+ """Adapt Headroom's TLS and subscription auth without changing its installed files."""
2
+
3
+ import contextlib
4
+ import os
5
+ import ssl
6
+ import urllib.error
7
+ import urllib.parse
8
+ import urllib.request
9
+ from collections.abc import Iterator
10
+ from dataclasses import dataclass
11
+ from http.client import HTTPException, HTTPResponse
12
+ from types import ModuleType
13
+
14
+ from headroom_kit.runtime import KitError
15
+
16
+ LOGIN_COMMAND = "headroom-kit copilot-auth login"
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class CopilotAuth:
21
+ api_url: str
22
+ refresh_oauth_token: str | None
23
+
24
+
25
+ def configure_urllib_tls() -> None:
26
+ from headroom.proxy import ssl_context
27
+
28
+ def build_context() -> ssl.SSLContext | None:
29
+ # Headroom 0.37.0 shares httpx's ALPN list with urllib, which cannot speak h2.
30
+ context = ssl_context.build_httpx_verify()
31
+ if isinstance(context, ssl.SSLContext):
32
+ context.set_alpn_protocols(["http/1.1"])
33
+ return context
34
+ return None
35
+
36
+ ssl_context.build_urlopen_context = build_context
37
+
38
+
39
+ def run_copilot_auth(args: list[str]) -> int:
40
+ """Run Headroom's Copilot auth in this interpreter."""
41
+ configure_urllib_tls()
42
+ try:
43
+ from click import ClickException
44
+ from headroom.cli.copilot_auth import copilot_auth
45
+ except ImportError:
46
+ raise KitError(
47
+ "This Headroom release lacks the required Copilot subscription API. "
48
+ "Install headroom-ai 0.37.0 in this environment."
49
+ ) from None
50
+ try:
51
+ code = copilot_auth.main(
52
+ args=args,
53
+ prog_name="headroom-kit copilot-auth",
54
+ standalone_mode=False,
55
+ )
56
+ except ClickException as exc:
57
+ exc.show()
58
+ return exc.exit_code
59
+ return 0 if code is None else int(code)
60
+
61
+
62
+ @contextlib.contextmanager
63
+ def auth_failures(adapter: ModuleType) -> Iterator[set[str]]:
64
+ upstream_urlopen = adapter._urlopen
65
+ failures: set[str] = set()
66
+
67
+ @contextlib.contextmanager
68
+ def urlopen(request: urllib.request.Request, *, timeout: float) -> Iterator[HTTPResponse]:
69
+ try:
70
+ with upstream_urlopen(request, timeout=timeout) as response:
71
+ yield response
72
+ except urllib.error.HTTPError as error:
73
+ if error.code not in (401, 403):
74
+ failures.add("service")
75
+ error.close()
76
+ # Upstream catches and logs exchange failures, including response bodies.
77
+ raise OSError("Copilot authorization HTTP request failed.") from None
78
+ except (OSError, HTTPException):
79
+ failures.add("transport")
80
+ raise OSError("Copilot authorization transport failed.") from None
81
+
82
+ adapter._urlopen = urlopen
83
+ try:
84
+ yield failures
85
+ finally:
86
+ adapter._urlopen = upstream_urlopen
87
+
88
+
89
+ def copilot_auth() -> CopilotAuth:
90
+ # No upstream wrapper lifecycle: it can restart proxies or edit normal config.
91
+ try:
92
+ from headroom import copilot_auth as adapter
93
+
94
+ configure_urllib_tls()
95
+ except ImportError:
96
+ raise KitError(
97
+ "This Headroom release lacks the required Copilot subscription API. "
98
+ "Install headroom-ai 0.37.0 in this environment."
99
+ ) from None
100
+ try:
101
+ with (
102
+ auth_failures(adapter) as failures,
103
+ open(os.devnull, "w") as sink,
104
+ contextlib.redirect_stdout(sink),
105
+ contextlib.redirect_stderr(sink),
106
+ ):
107
+ resolution = adapter.resolve_subscription_bearer_token_details()
108
+ if resolution is None:
109
+ if failures:
110
+ failure = "transport" if "transport" in failures else "service"
111
+ raise KitError(
112
+ f"Copilot authorization {failure} failed. "
113
+ "Check network, proxy, CA settings, and GitHub service availability; retry. "
114
+ "This does not establish that your saved credentials were rejected."
115
+ )
116
+ raise ValueError
117
+ parsed = urllib.parse.urlsplit(resolution.api_url)
118
+ if (
119
+ parsed.scheme != "https"
120
+ or not (parsed.hostname or "").endswith(".githubcopilot.com")
121
+ or parsed.username
122
+ or parsed.password
123
+ or parsed.query
124
+ or parsed.fragment
125
+ ):
126
+ raise ValueError
127
+ if not resolution.refresh_oauth_token:
128
+ raise ValueError
129
+ # Pin the reusable credential, not a rotating access token.
130
+ return CopilotAuth(
131
+ api_url=resolution.api_url,
132
+ refresh_oauth_token=resolution.refresh_oauth_token,
133
+ )
134
+ except (AttributeError, OSError, RuntimeError, TypeError, ValueError):
135
+ raise KitError(
136
+ "Copilot subscription authorization failed or its endpoint is unsupported. "
137
+ f"Run `{LOGIN_COMMAND}`, check your subscription, and retry. "
138
+ "Shared proxies require reusable OAuth and HTTPS *.githubcopilot.com upstreams."
139
+ ) from None
140
+
141
+
142
+ def managed_copilot_auth() -> None:
143
+ from headroom import copilot_auth as adapter
144
+
145
+ configure_urllib_tls()
146
+ upstream_auth = adapter.apply_copilot_api_auth
147
+
148
+ async def authenticate(headers: dict[str, str], *, url: str) -> dict[str, str]:
149
+ # CLI placeholders and editor tokens both use this proxy's pinned account.
150
+ if adapter.is_copilot_upstream_url(url):
151
+ headers = {
152
+ k: v for k, v in headers.items() if k.lower() not in ("authorization", "x-api-key")
153
+ }
154
+ return await upstream_auth(headers, url=url)
155
+
156
+ adapter.apply_copilot_api_auth = authenticate