toolgate-io 0.3.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.
toolgate/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Toolgate: capability control plane for embedded AI agents."""
2
+
3
+ __version__ = "0.3.0"
@@ -0,0 +1 @@
1
+ """Toolgate operator CLI — a pure client of the control-plane API."""
toolgate/cli/client.py ADDED
@@ -0,0 +1,49 @@
1
+ from typing import Any
2
+
3
+ import httpx
4
+ import typer
5
+ from rich.console import Console
6
+
7
+ from .config import Profile
8
+
9
+ err_console = Console(stderr=True)
10
+
11
+
12
+ class AdminClient:
13
+ """Thin, honest wrapper over the control-plane API. Any non-2xx response is
14
+ rendered as the server's error envelope and exits non-zero."""
15
+
16
+ def __init__(self, profile: Profile) -> None:
17
+ self.url = profile.url
18
+ self._http = httpx.Client(
19
+ base_url=profile.url,
20
+ headers={"x-toolgate-admin-key": profile.admin_key},
21
+ timeout=15.0,
22
+ )
23
+
24
+ def get(self, path: str, **params: Any) -> Any:
25
+ return self._handle(
26
+ self._http.get(path, params={k: v for k, v in params.items() if v is not None})
27
+ )
28
+
29
+ def post(self, path: str, body: dict[str, Any] | None = None) -> Any:
30
+ return self._handle(self._http.post(path, json=body or {}))
31
+
32
+ def public(self, path: str) -> Any:
33
+ # Unauthenticated endpoints (/healthz, /v1/keys).
34
+ return self._handle(httpx.get(f"{self.url}{path}", timeout=15.0))
35
+
36
+ def _handle(self, res: httpx.Response) -> Any:
37
+ try:
38
+ body = res.json()
39
+ except ValueError:
40
+ body = {"error": {"code": "TG_INTERNAL", "message": res.text[:200]}}
41
+ if res.status_code >= 400:
42
+ err = body.get("error", {})
43
+ err_console.print(
44
+ f"[bold red]{err.get('code', res.status_code)}[/] {err.get('message', '')}"
45
+ )
46
+ if err.get("details"):
47
+ err_console.print(f"[dim]{err['details']}[/]")
48
+ raise typer.Exit(1)
49
+ return body
toolgate/cli/config.py ADDED
@@ -0,0 +1,48 @@
1
+ import json
2
+ import os
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+
7
+ def config_path() -> Path:
8
+ return Path(os.environ.get("TOOLGATE_CONFIG", "~/.toolgate/config.json")).expanduser()
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class Profile:
13
+ url: str
14
+ admin_key: str
15
+
16
+
17
+ def load_profiles() -> dict[str, dict[str, str]]:
18
+ path = config_path()
19
+ if not path.exists():
20
+ return {}
21
+ return json.loads(path.read_text())
22
+
23
+
24
+ def save_profile(name: str, url: str, admin_key: str) -> Path:
25
+ path = config_path()
26
+ profiles = load_profiles()
27
+ profiles[name] = {"url": url.rstrip("/"), "admin_key": admin_key}
28
+ path.parent.mkdir(parents=True, exist_ok=True)
29
+ path.write_text(json.dumps(profiles, indent=2) + "\n")
30
+ path.chmod(0o600)
31
+ return path
32
+
33
+
34
+ def resolve(profile: str | None) -> Profile:
35
+ """Environment wins (CI/scripting); otherwise the named or default profile."""
36
+ env_url = os.environ.get("TOOLGATE_URL")
37
+ env_key = os.environ.get("TOOLGATE_ADMIN_KEY")
38
+ if env_url and env_key:
39
+ return Profile(url=env_url.rstrip("/"), admin_key=env_key)
40
+
41
+ profiles = load_profiles()
42
+ name = profile or "default"
43
+ if name not in profiles:
44
+ raise LookupError(
45
+ f"no profile '{name}' — run `toolgate init` or set TOOLGATE_URL and TOOLGATE_ADMIN_KEY"
46
+ )
47
+ entry = profiles[name]
48
+ return Profile(url=entry["url"], admin_key=entry["admin_key"])