haliosai-cli 2.0.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.
halios_cli/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Halios command-line client for coding-agent reliability workflows."""
2
+
3
+ from ._version import __version__
4
+
5
+ __all__ = ["__version__"]
halios_cli/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "2.0.0"
halios_cli/cli.py ADDED
@@ -0,0 +1,47 @@
1
+ """Opinionated Halios CLI entry point."""
2
+
3
+ import typer
4
+
5
+ from ._version import __version__
6
+ from .cli_auth import app as auth_app
7
+ from .cli_eval import app as eval_app
8
+ from .cli_optimize import app as optimize_app
9
+ from .cli_project import app as project_app
10
+ from .cli_scenario import app as scenario_app
11
+ from .cli_trace import app as trace_app
12
+
13
+ app = typer.Typer(
14
+ name="halios",
15
+ help="Scenario simulations and reliability gates for AI agents.",
16
+ no_args_is_help=True,
17
+ add_completion=False,
18
+ )
19
+ app.add_typer(auth_app, name="auth")
20
+ app.add_typer(project_app, name="project")
21
+ app.add_typer(eval_app, name="eval")
22
+ app.add_typer(scenario_app, name="scenario")
23
+ app.add_typer(trace_app, name="trace")
24
+ app.add_typer(optimize_app, name="optimize")
25
+
26
+
27
+ def _version_callback(value: bool) -> None:
28
+ if value:
29
+ typer.echo(__version__)
30
+ raise typer.Exit()
31
+
32
+
33
+ @app.callback()
34
+ def main(
35
+ version: bool = typer.Option(
36
+ False,
37
+ "--version",
38
+ callback=_version_callback,
39
+ is_eager=True,
40
+ help="Show the installed Halios CLI version.",
41
+ ),
42
+ ) -> None:
43
+ """Scenario simulations and reliability gates for AI agents."""
44
+
45
+
46
+ if __name__ == "__main__":
47
+ app()
halios_cli/cli_auth.py ADDED
@@ -0,0 +1,162 @@
1
+ """User-level CLI authentication commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import secrets
7
+ import threading
8
+ import urllib.parse
9
+ import webbrowser
10
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
11
+
12
+ import httpx
13
+ import typer
14
+
15
+ from .cli_support import (
16
+ DEFAULT_BASE_URL,
17
+ ApiClient,
18
+ ApiError,
19
+ delete_profile,
20
+ normalize_url,
21
+ resolve_credentials,
22
+ save_profile,
23
+ stored_profile_credentials,
24
+ )
25
+
26
+ app = typer.Typer(help="Authenticate the Halios CLI.", no_args_is_help=True)
27
+
28
+
29
+ @app.command("login")
30
+ def login(
31
+ profile: str = typer.Option("default", "--profile"),
32
+ base_url: str = typer.Option(DEFAULT_BASE_URL, "--base-url"),
33
+ api_key: str | None = typer.Option(None, "--api-key", envvar="HALIOS_API_KEY", hidden=True),
34
+ ) -> None:
35
+ """Authorize this machine and store credentials outside the repository."""
36
+ normalized_url = normalize_url(base_url)
37
+ if api_key:
38
+ save_profile(profile, base_url=normalized_url, api_key=api_key)
39
+ typer.echo(f"Logged in as profile '{profile}'.")
40
+ return
41
+
42
+ result: dict[str, str] = {}
43
+ completed = threading.Event()
44
+ state = secrets.token_urlsafe(24)
45
+
46
+ class CallbackHandler(BaseHTTPRequestHandler):
47
+ def _cors(self) -> None:
48
+ self.send_header("Access-Control-Allow-Origin", normalized_url)
49
+ self.send_header("Access-Control-Allow-Methods", "POST, OPTIONS")
50
+ self.send_header("Access-Control-Allow-Headers", "Content-Type")
51
+
52
+ def do_OPTIONS(self) -> None: # noqa: N802
53
+ self.send_response(204)
54
+ self._cors()
55
+ self.end_headers()
56
+
57
+ def do_POST(self) -> None: # noqa: N802
58
+ if self.path != "/callback":
59
+ self.send_error(404)
60
+ return
61
+ length = int(self.headers.get("content-length") or 0)
62
+ try:
63
+ body = json.loads(self.rfile.read(length))
64
+ except (ValueError, UnicodeDecodeError):
65
+ self.send_error(400)
66
+ return
67
+ if not isinstance(body, dict) or not secrets.compare_digest(
68
+ str(body.get("state") or ""), state
69
+ ):
70
+ self.send_error(422)
71
+ return
72
+ if body.get("error"):
73
+ result["error"] = str(body["error"])
74
+ elif body.get("api_key"):
75
+ result.update({key: str(value) for key, value in body.items() if value is not None})
76
+ else:
77
+ self.send_error(422)
78
+ return
79
+ self.send_response(204)
80
+ self._cors()
81
+ self.end_headers()
82
+ completed.set()
83
+
84
+ def log_message(self, _format: str, *_args: object) -> None:
85
+ return
86
+
87
+ server = ThreadingHTTPServer(("127.0.0.1", 0), CallbackHandler)
88
+ thread = threading.Thread(target=server.serve_forever, daemon=True)
89
+ thread.start()
90
+ login_url = f"{normalized_url}/cli-login?" + urllib.parse.urlencode(
91
+ {"port": server.server_port, "profile": profile, "state": state}
92
+ )
93
+ typer.echo(f"Open this URL to authorize the CLI:\n{login_url}")
94
+ webbrowser.open(login_url)
95
+ try:
96
+ if not completed.wait(timeout=300):
97
+ raise typer.BadParameter("Login timed out after 5 minutes")
98
+ finally:
99
+ server.shutdown()
100
+ server.server_close()
101
+
102
+ if result.get("error") == "access_denied":
103
+ raise typer.BadParameter("Authorization was cancelled in the browser")
104
+ if result.get("error"):
105
+ raise typer.BadParameter(f"Authorization failed: {result['error']}")
106
+
107
+ save_profile(
108
+ profile,
109
+ base_url=normalized_url,
110
+ api_key=result["api_key"],
111
+ organization_id=result.get("organization_id"),
112
+ api_key_id=int(result["api_key_id"]) if result.get("api_key_id") else None,
113
+ expires_at=result.get("expires_at"),
114
+ )
115
+ typer.echo(f"Logged in as profile '{profile}'.")
116
+
117
+
118
+ @app.command("status")
119
+ def status(profile: str = typer.Option("default", "--profile")) -> None:
120
+ """Verify the selected profile without exposing its credential."""
121
+ credentials = resolve_credentials(profile)
122
+ with ApiClient(credentials) as api:
123
+ api.request("GET", "/api/v1/agents", params={"limit": 1})
124
+ expiry = f" Credential expires {credentials.expires_at}." if credentials.expires_at else ""
125
+ typer.echo(f"Authenticated profile '{profile}' at {credentials.base_url}.{expiry}")
126
+
127
+
128
+ @app.command("logout")
129
+ def logout(
130
+ profile: str = typer.Option("default", "--profile"),
131
+ local_only: bool = typer.Option(
132
+ False,
133
+ "--local-only",
134
+ help="Remove local credentials without revoking the remote key.",
135
+ ),
136
+ ) -> None:
137
+ """Revoke one CLI credential, then remove its local profile."""
138
+ credentials = stored_profile_credentials(profile)
139
+ if credentials is None:
140
+ typer.echo(f"Profile '{profile}' was not stored.")
141
+ return
142
+
143
+ if not local_only:
144
+ try:
145
+ with ApiClient(credentials) as api:
146
+ api.request("DELETE", "/api/v1/api-keys/current")
147
+ except ApiError as exc:
148
+ if exc.status_code != 401:
149
+ raise typer.BadParameter(
150
+ "Remote revocation failed; local credentials were kept. "
151
+ "Retry, or use --local-only if the server is permanently unavailable."
152
+ ) from exc
153
+ typer.echo("The remote credential was already invalid or expired.")
154
+ except httpx.HTTPError as exc:
155
+ raise typer.BadParameter(
156
+ "Could not reach Halios to revoke the credential; local credentials were kept. "
157
+ "Retry, or use --local-only if the server is permanently unavailable."
158
+ ) from exc
159
+
160
+ delete_profile(profile)
161
+ suffix = " locally (remote key unchanged)" if local_only else " and revoked its API key"
162
+ typer.echo(f"Removed profile '{profile}'{suffix}.")