hydracept 0.1.0__tar.gz
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.
- hydracept-0.1.0/PKG-INFO +48 -0
- hydracept-0.1.0/README.md +32 -0
- hydracept-0.1.0/hydracept/__init__.py +115 -0
- hydracept-0.1.0/hydracept/cli/__init__.py +1 -0
- hydracept-0.1.0/hydracept/cli/main.py +433 -0
- hydracept-0.1.0/hydracept/consumer_boundary.py +109 -0
- hydracept-0.1.0/hydracept/data/__init__.py +0 -0
- hydracept-0.1.0/hydracept/data/hydracept-consumer-exceptions.yaml +4 -0
- hydracept-0.1.0/hydracept.egg-info/PKG-INFO +48 -0
- hydracept-0.1.0/hydracept.egg-info/SOURCES.txt +14 -0
- hydracept-0.1.0/hydracept.egg-info/dependency_links.txt +1 -0
- hydracept-0.1.0/hydracept.egg-info/entry_points.txt +2 -0
- hydracept-0.1.0/hydracept.egg-info/requires.txt +4 -0
- hydracept-0.1.0/hydracept.egg-info/top_level.txt +1 -0
- hydracept-0.1.0/pyproject.toml +33 -0
- hydracept-0.1.0/setup.cfg +4 -0
hydracept-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hydracept
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client and CLI for the Hydracept public API — AI execution infrastructure for games.
|
|
5
|
+
Author: Zencode
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://hydracept.com
|
|
8
|
+
Project-URL: Documentation, https://hydracept.com/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/hydracept/hydracept-public
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: httpx>=0.27.0
|
|
13
|
+
Requires-Dist: typer>=0.15.0
|
|
14
|
+
Requires-Dist: rich>=13.9.0
|
|
15
|
+
Requires-Dist: PyYAML>=6.0
|
|
16
|
+
|
|
17
|
+
# hydracept
|
|
18
|
+
|
|
19
|
+
Python client for the Hydracept public API — AI execution infrastructure for games.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install hydracept
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from hydracept import HydraceptClient
|
|
27
|
+
|
|
28
|
+
client = HydraceptClient("https://api.hydracept.com", token="...")
|
|
29
|
+
job = client.submit_capability_job(
|
|
30
|
+
"image.generate.v1",
|
|
31
|
+
{
|
|
32
|
+
"context": {
|
|
33
|
+
"productId": "my-product",
|
|
34
|
+
"projectId": "cpr_...",
|
|
35
|
+
"environment": "development",
|
|
36
|
+
},
|
|
37
|
+
"input": {"prompt": "cute slime icon"},
|
|
38
|
+
"execution": {"executionPreference": "automatic"},
|
|
39
|
+
"idempotencyKey": "demo-1",
|
|
40
|
+
},
|
|
41
|
+
)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
CLI: `hydracept login`, `hydracept init`, `hydracept doctor`
|
|
45
|
+
|
|
46
|
+
Docs: https://docs.hydracept.com
|
|
47
|
+
|
|
48
|
+
A Zencode product · © Zencode Consulting Inc.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# hydracept
|
|
2
|
+
|
|
3
|
+
Python client for the Hydracept public API — AI execution infrastructure for games.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install hydracept
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from hydracept import HydraceptClient
|
|
11
|
+
|
|
12
|
+
client = HydraceptClient("https://api.hydracept.com", token="...")
|
|
13
|
+
job = client.submit_capability_job(
|
|
14
|
+
"image.generate.v1",
|
|
15
|
+
{
|
|
16
|
+
"context": {
|
|
17
|
+
"productId": "my-product",
|
|
18
|
+
"projectId": "cpr_...",
|
|
19
|
+
"environment": "development",
|
|
20
|
+
},
|
|
21
|
+
"input": {"prompt": "cute slime icon"},
|
|
22
|
+
"execution": {"executionPreference": "automatic"},
|
|
23
|
+
"idempotencyKey": "demo-1",
|
|
24
|
+
},
|
|
25
|
+
)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
CLI: `hydracept login`, `hydracept init`, `hydracept doctor`
|
|
29
|
+
|
|
30
|
+
Docs: https://docs.hydracept.com
|
|
31
|
+
|
|
32
|
+
A Zencode product · © Zencode Consulting Inc.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Hydracept Python client — public API surface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, Iterator
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class HydraceptClient:
|
|
12
|
+
def __init__(self, base_url: str, token: str, *, timeout: float = 120.0) -> None:
|
|
13
|
+
self.base_url = base_url.rstrip("/")
|
|
14
|
+
self._headers = {
|
|
15
|
+
"Authorization": f"Bearer {token}",
|
|
16
|
+
"Content-Type": "application/json",
|
|
17
|
+
}
|
|
18
|
+
self._timeout = timeout
|
|
19
|
+
|
|
20
|
+
def _get(self, path: str) -> dict[str, Any]:
|
|
21
|
+
response = httpx.get(
|
|
22
|
+
f"{self.base_url}{path}",
|
|
23
|
+
headers=self._headers,
|
|
24
|
+
timeout=self._timeout,
|
|
25
|
+
)
|
|
26
|
+
response.raise_for_status()
|
|
27
|
+
return response.json()
|
|
28
|
+
|
|
29
|
+
def _post(self, path: str, body: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
30
|
+
response = httpx.post(
|
|
31
|
+
f"{self.base_url}{path}",
|
|
32
|
+
headers=self._headers,
|
|
33
|
+
json=body,
|
|
34
|
+
timeout=self._timeout,
|
|
35
|
+
)
|
|
36
|
+
response.raise_for_status()
|
|
37
|
+
if not response.content:
|
|
38
|
+
return {}
|
|
39
|
+
return response.json()
|
|
40
|
+
|
|
41
|
+
def capabilities(self) -> dict[str, Any]:
|
|
42
|
+
return self._get("/v1/capabilities")
|
|
43
|
+
|
|
44
|
+
def describe_capability(self, key: str) -> dict[str, Any]:
|
|
45
|
+
return self._get(f"/v1/capabilities/{key}")
|
|
46
|
+
|
|
47
|
+
def invoke_capability(self, key: str, body: dict[str, Any]) -> dict[str, Any]:
|
|
48
|
+
return self._post(f"/v1/capabilities/{key}/invoke", body)
|
|
49
|
+
|
|
50
|
+
def submit_capability_job(self, key: str, body: dict[str, Any]) -> dict[str, Any]:
|
|
51
|
+
return self._post(f"/v1/capabilities/{key}/jobs", body)
|
|
52
|
+
|
|
53
|
+
def get_job(self, job_id: str) -> dict[str, Any]:
|
|
54
|
+
return self._get(f"/v1/jobs/{job_id}")
|
|
55
|
+
|
|
56
|
+
def get_job_receipt(self, job_id: str) -> dict[str, Any]:
|
|
57
|
+
return self._get(f"/v1/jobs/{job_id}/receipt")
|
|
58
|
+
|
|
59
|
+
def cancel_job(self, job_id: str) -> dict[str, Any]:
|
|
60
|
+
return self._post(f"/v1/jobs/{job_id}/cancel")
|
|
61
|
+
|
|
62
|
+
def agent_context(self) -> dict[str, Any]:
|
|
63
|
+
return self._get("/v1/agent-context")
|
|
64
|
+
|
|
65
|
+
def diagnostics_session(self) -> dict[str, Any]:
|
|
66
|
+
return self._get("/v1/diagnostics/session")
|
|
67
|
+
|
|
68
|
+
def policy_dry_run(self, body: dict[str, Any]) -> dict[str, Any]:
|
|
69
|
+
return self._post("/v1/policy-evaluations", body)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def iter_invocation_events(
|
|
73
|
+
base_url: str,
|
|
74
|
+
token: str,
|
|
75
|
+
execution_id: str,
|
|
76
|
+
*,
|
|
77
|
+
last_event_id: str | None = None,
|
|
78
|
+
) -> Iterator[dict[str, Any]]:
|
|
79
|
+
"""Maintained SSE helper for legacy invocation streams."""
|
|
80
|
+
headers = {
|
|
81
|
+
"Authorization": f"Bearer {token}",
|
|
82
|
+
"Accept": "text/event-stream",
|
|
83
|
+
}
|
|
84
|
+
if last_event_id:
|
|
85
|
+
headers["Last-Event-ID"] = last_event_id
|
|
86
|
+
with httpx.stream(
|
|
87
|
+
"GET",
|
|
88
|
+
f"{base_url.rstrip('/')}/v1/invocations/{execution_id}/events",
|
|
89
|
+
headers=headers,
|
|
90
|
+
timeout=None,
|
|
91
|
+
) as response:
|
|
92
|
+
response.raise_for_status()
|
|
93
|
+
event_name = "message"
|
|
94
|
+
data_lines: list[str] = []
|
|
95
|
+
event_id: str | None = None
|
|
96
|
+
for line in response.iter_lines():
|
|
97
|
+
if line == "":
|
|
98
|
+
if data_lines:
|
|
99
|
+
yield {
|
|
100
|
+
"id": event_id,
|
|
101
|
+
"event": event_name,
|
|
102
|
+
"data": json.loads("\n".join(data_lines)),
|
|
103
|
+
}
|
|
104
|
+
event_name = "message"
|
|
105
|
+
data_lines = []
|
|
106
|
+
event_id = None
|
|
107
|
+
continue
|
|
108
|
+
if line.startswith(":"):
|
|
109
|
+
continue
|
|
110
|
+
if line.startswith("id:"):
|
|
111
|
+
event_id = line[3:].strip()
|
|
112
|
+
elif line.startswith("event:"):
|
|
113
|
+
event_name = line[6:].strip()
|
|
114
|
+
elif line.startswith("data:"):
|
|
115
|
+
data_lines.append(line[5:].lstrip())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Package marker for public Hydracept CLI."""
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
"""Public Hydracept CLI — device login, init, doctor, capabilities, jobs, consumer-check."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import stat
|
|
8
|
+
import time
|
|
9
|
+
import webbrowser
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
import typer
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
|
|
17
|
+
from hydracept import HydraceptClient
|
|
18
|
+
from hydracept.consumer_boundary import scan_path
|
|
19
|
+
|
|
20
|
+
app = typer.Typer(help="Hydracept public CLI — AI execution infrastructure for games.")
|
|
21
|
+
capabilities_app = typer.Typer(help="Capability discovery and invoke")
|
|
22
|
+
jobs_app = typer.Typer(help="Durable jobs")
|
|
23
|
+
app.add_typer(capabilities_app, name="capabilities")
|
|
24
|
+
app.add_typer(jobs_app, name="jobs")
|
|
25
|
+
console = Console()
|
|
26
|
+
|
|
27
|
+
DEFAULT_API = os.environ.get("HYDRACEPT_API_URL", "https://api.hydracept.com")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _config_dir(project_root: Path) -> Path:
|
|
31
|
+
return project_root / ".hydracept"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _secrets_path(project_root: Path) -> Path:
|
|
35
|
+
return _config_dir(project_root) / "secrets.json"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _config_path(project_root: Path) -> Path:
|
|
39
|
+
return _config_dir(project_root) / "config.json"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _read_json(path: Path) -> dict[str, Any]:
|
|
43
|
+
if not path.is_file():
|
|
44
|
+
return {}
|
|
45
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _write_secrets(project_root: Path, payload: dict[str, Any]) -> Path:
|
|
49
|
+
path = _secrets_path(project_root)
|
|
50
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
existing = _read_json(path)
|
|
52
|
+
existing.update(payload)
|
|
53
|
+
path.write_text(json.dumps(existing, indent=2) + "\n", encoding="utf-8")
|
|
54
|
+
try:
|
|
55
|
+
path.chmod(stat.S_IRUSR | stat.S_IWUSR)
|
|
56
|
+
except OSError:
|
|
57
|
+
pass
|
|
58
|
+
return path
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _ensure_gitignore(project_root: Path) -> None:
|
|
62
|
+
gitignore = project_root / ".gitignore"
|
|
63
|
+
hints = [".hydracept/secrets.json", ".hydracept/*.env", ".env.hydracept"]
|
|
64
|
+
existing = gitignore.read_text(encoding="utf-8") if gitignore.exists() else ""
|
|
65
|
+
additions = [h for h in hints if h not in existing]
|
|
66
|
+
if not additions:
|
|
67
|
+
return
|
|
68
|
+
with gitignore.open("a", encoding="utf-8") as handle:
|
|
69
|
+
handle.write("\n# Hydracept local secrets\n")
|
|
70
|
+
for hint in additions:
|
|
71
|
+
handle.write(f"{hint}\n")
|
|
72
|
+
console.print(f"[green]Updated[/green] {gitignore}")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _resolve_token(project_root: Path, token: str | None) -> str:
|
|
76
|
+
if token:
|
|
77
|
+
return token
|
|
78
|
+
env = (
|
|
79
|
+
os.environ.get("HYDRACEPT_API_KEY")
|
|
80
|
+
or os.environ.get("HYDRACEPT_TOKEN")
|
|
81
|
+
or ""
|
|
82
|
+
).strip()
|
|
83
|
+
if env:
|
|
84
|
+
return env
|
|
85
|
+
secrets = _read_json(_secrets_path(project_root))
|
|
86
|
+
# Prefer project API credential over human session token
|
|
87
|
+
return str(secrets.get("apiKey") or secrets.get("token") or "")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _auth_headers(token: str) -> dict[str, str]:
|
|
91
|
+
return {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@app.command("login")
|
|
95
|
+
def login_cmd(
|
|
96
|
+
api: str = typer.Option(DEFAULT_API, "--api"),
|
|
97
|
+
project_root: Path = typer.Option(Path.cwd(), "--project-root"),
|
|
98
|
+
open_browser: bool = typer.Option(True, "--open/--no-open"),
|
|
99
|
+
) -> None:
|
|
100
|
+
"""Authenticate a human via device authorization (headless-friendly)."""
|
|
101
|
+
start = httpx.post(f"{api.rstrip('/')}/v1/auth/device/start", timeout=30.0)
|
|
102
|
+
start.raise_for_status()
|
|
103
|
+
payload = start.json()
|
|
104
|
+
user_code = payload.get("userCode") or payload.get("user_code")
|
|
105
|
+
verification = (
|
|
106
|
+
payload.get("verificationUri")
|
|
107
|
+
or payload.get("verification_uri")
|
|
108
|
+
or f"{os.environ.get('HYDRACEPT_SITE_URL', 'https://hydracept.com')}/activate"
|
|
109
|
+
)
|
|
110
|
+
console.print("[bold]Starting Hydracept authentication...[/bold]")
|
|
111
|
+
console.print(f"Open: [cyan]{verification}[/cyan]")
|
|
112
|
+
console.print(f"Code: [bold]{user_code}[/bold]")
|
|
113
|
+
if open_browser:
|
|
114
|
+
try:
|
|
115
|
+
webbrowser.open(str(verification))
|
|
116
|
+
except Exception:
|
|
117
|
+
pass
|
|
118
|
+
device_code = payload.get("deviceCode") or payload.get("device_code")
|
|
119
|
+
while True:
|
|
120
|
+
token_resp = httpx.post(
|
|
121
|
+
f"{api.rstrip('/')}/v1/auth/device/token",
|
|
122
|
+
json={"deviceCode": device_code},
|
|
123
|
+
timeout=30.0,
|
|
124
|
+
)
|
|
125
|
+
if token_resp.status_code == 428:
|
|
126
|
+
time.sleep(2)
|
|
127
|
+
continue
|
|
128
|
+
token_resp.raise_for_status()
|
|
129
|
+
data = token_resp.json()
|
|
130
|
+
token = data.get("token")
|
|
131
|
+
if not token:
|
|
132
|
+
console.print("[red]No token in device response[/red]")
|
|
133
|
+
raise typer.Exit(1)
|
|
134
|
+
_write_secrets(project_root, {"token": token, "kind": "human_session"})
|
|
135
|
+
_ensure_gitignore(project_root)
|
|
136
|
+
console.print("[green]Signed in[/green] (human session). Run [bold]hydracept init[/bold] next.")
|
|
137
|
+
break
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@app.command("init")
|
|
141
|
+
def init_cmd(
|
|
142
|
+
apply: bool = typer.Option(False, "--apply"),
|
|
143
|
+
yes: bool = typer.Option(False, "--yes"),
|
|
144
|
+
print_env: bool = typer.Option(False, "--print-env"),
|
|
145
|
+
rotate: bool = typer.Option(False, "--rotate"),
|
|
146
|
+
api: str = typer.Option(DEFAULT_API, "--api"),
|
|
147
|
+
project_root: Path = typer.Option(Path.cwd(), "--project-root"),
|
|
148
|
+
) -> None:
|
|
149
|
+
"""Idempotent project bootstrap. login = human; init = service principal."""
|
|
150
|
+
config_dir = _config_dir(project_root)
|
|
151
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
152
|
+
config_path = _config_path(project_root)
|
|
153
|
+
secrets = _read_json(_secrets_path(project_root))
|
|
154
|
+
human_token = str(secrets.get("token") or "")
|
|
155
|
+
if not human_token:
|
|
156
|
+
console.print("[red]No human session. Run hydracept login first.[/red]")
|
|
157
|
+
raise typer.Exit(1)
|
|
158
|
+
|
|
159
|
+
config: dict[str, Any] = {
|
|
160
|
+
"apiBaseUrl": api.rstrip("/"),
|
|
161
|
+
"environment": "development",
|
|
162
|
+
"detectedStack": _detect_stack(project_root),
|
|
163
|
+
}
|
|
164
|
+
if config_path.exists():
|
|
165
|
+
config = {**_read_json(config_path), **config}
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
context = httpx.get(
|
|
169
|
+
f"{api.rstrip('/')}/v1/session/context",
|
|
170
|
+
headers=_auth_headers(human_token),
|
|
171
|
+
timeout=30.0,
|
|
172
|
+
)
|
|
173
|
+
if context.status_code == 200:
|
|
174
|
+
ctx = context.json()
|
|
175
|
+
if ctx.get("productId"):
|
|
176
|
+
config["productId"] = ctx["productId"]
|
|
177
|
+
env = ctx.get("environment")
|
|
178
|
+
if isinstance(env, dict) and env.get("slug"):
|
|
179
|
+
config["environment"] = env["slug"]
|
|
180
|
+
project = ctx.get("project")
|
|
181
|
+
if isinstance(project, dict):
|
|
182
|
+
config["projectId"] = project.get("id")
|
|
183
|
+
config["projectName"] = project.get("displayName")
|
|
184
|
+
org = ctx.get("organization")
|
|
185
|
+
if isinstance(org, dict):
|
|
186
|
+
config["organizationId"] = org.get("id")
|
|
187
|
+
config["organizationName"] = org.get("displayName")
|
|
188
|
+
except httpx.HTTPError as exc:
|
|
189
|
+
console.print(f"[yellow]session/context unavailable:[/yellow] {exc}")
|
|
190
|
+
|
|
191
|
+
config_path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
|
|
192
|
+
console.print(f"[green]Wrote[/green] {config_path}")
|
|
193
|
+
|
|
194
|
+
existing_key = str(secrets.get("apiKey") or "")
|
|
195
|
+
if existing_key and not rotate and apply:
|
|
196
|
+
console.print("[green]Reusing existing project API credential[/green]")
|
|
197
|
+
if print_env:
|
|
198
|
+
console.print(f"HYDRACEPT_API_KEY={existing_key}")
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
if not apply:
|
|
202
|
+
console.print("Run with --apply to create/select a project API credential.")
|
|
203
|
+
return
|
|
204
|
+
if not yes:
|
|
205
|
+
console.print("Refusing --apply without --yes")
|
|
206
|
+
raise typer.Exit(1)
|
|
207
|
+
|
|
208
|
+
# Prefer Free bootstrap when org missing
|
|
209
|
+
api_key: str | None = existing_key or None
|
|
210
|
+
if not api_key or rotate:
|
|
211
|
+
bootstrap = httpx.post(
|
|
212
|
+
f"{api.rstrip('/')}/v1/onboarding/bootstrap-free",
|
|
213
|
+
headers=_auth_headers(human_token),
|
|
214
|
+
json={},
|
|
215
|
+
timeout=60.0,
|
|
216
|
+
)
|
|
217
|
+
if bootstrap.status_code < 400:
|
|
218
|
+
body = bootstrap.json()
|
|
219
|
+
api_key = (
|
|
220
|
+
body.get("apiKey")
|
|
221
|
+
or body.get("token")
|
|
222
|
+
or (body.get("credential") or {}).get("secret")
|
|
223
|
+
)
|
|
224
|
+
if body.get("projectId"):
|
|
225
|
+
config["projectId"] = body["projectId"]
|
|
226
|
+
if body.get("organizationId"):
|
|
227
|
+
config["organizationId"] = body["organizationId"]
|
|
228
|
+
config_path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
|
|
229
|
+
elif not api_key:
|
|
230
|
+
console.print(
|
|
231
|
+
f"[red]bootstrap-free failed ({bootstrap.status_code}). "
|
|
232
|
+
"Create an API key in Studio or retry after Free activation.[/red]"
|
|
233
|
+
)
|
|
234
|
+
raise typer.Exit(1)
|
|
235
|
+
|
|
236
|
+
if api_key:
|
|
237
|
+
secrets_path = _write_secrets(
|
|
238
|
+
project_root,
|
|
239
|
+
{"apiKey": api_key, "kind": "service_principal"},
|
|
240
|
+
)
|
|
241
|
+
_ensure_gitignore(project_root)
|
|
242
|
+
env_path = config_dir / "local.env"
|
|
243
|
+
env_path.write_text(
|
|
244
|
+
f"HYDRACEPT_API_URL={api.rstrip('/')}\n"
|
|
245
|
+
f"HYDRACEPT_API_KEY={api_key}\n"
|
|
246
|
+
f"HYDRACEPT_PROJECT={config.get('projectId') or ''}\n"
|
|
247
|
+
f"HYDRACEPT_ENVIRONMENT={config.get('environment') or 'development'}\n",
|
|
248
|
+
encoding="utf-8",
|
|
249
|
+
)
|
|
250
|
+
try:
|
|
251
|
+
env_path.chmod(stat.S_IRUSR | stat.S_IWUSR)
|
|
252
|
+
except OSError:
|
|
253
|
+
pass
|
|
254
|
+
console.print(f"[green]Wrote credential to[/green] {secrets_path} and {env_path}")
|
|
255
|
+
if print_env:
|
|
256
|
+
console.print(f"HYDRACEPT_API_KEY={api_key}")
|
|
257
|
+
else:
|
|
258
|
+
console.print("[dim]API key not printed (use --print-env to display).[/dim]")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _detect_stack(project_root: Path) -> str:
|
|
262
|
+
is_dotnet = any(project_root.glob("*.csproj")) or (project_root / "Assets").exists()
|
|
263
|
+
is_unity = (project_root / "Assets").exists() and (project_root / "ProjectSettings").exists()
|
|
264
|
+
if is_unity:
|
|
265
|
+
return "unity"
|
|
266
|
+
if is_dotnet:
|
|
267
|
+
return "dotnet"
|
|
268
|
+
if (project_root / "package.json").exists():
|
|
269
|
+
return "node"
|
|
270
|
+
if (project_root / "pyproject.toml").exists() or (project_root / "requirements.txt").exists():
|
|
271
|
+
return "python"
|
|
272
|
+
return "unknown"
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@app.command("doctor")
|
|
276
|
+
def doctor_cmd(
|
|
277
|
+
api: str = typer.Option(DEFAULT_API, "--api"),
|
|
278
|
+
project_root: Path = typer.Option(Path.cwd(), "--project-root"),
|
|
279
|
+
token: str = typer.Option("", "--token"),
|
|
280
|
+
) -> None:
|
|
281
|
+
resolved = _resolve_token(project_root, token or None)
|
|
282
|
+
if not resolved:
|
|
283
|
+
console.print("[red]No API credential. Run hydracept login && hydracept init --apply --yes[/red]")
|
|
284
|
+
raise typer.Exit(1)
|
|
285
|
+
client = HydraceptClient(api, resolved)
|
|
286
|
+
session = client.diagnostics_session()
|
|
287
|
+
console.print("[green]diagnostics/session[/green]")
|
|
288
|
+
console.print_json(data=session)
|
|
289
|
+
try:
|
|
290
|
+
providers = httpx.get(
|
|
291
|
+
f"{api.rstrip('/')}/v1/diagnostics/providers",
|
|
292
|
+
headers=_auth_headers(resolved),
|
|
293
|
+
timeout=30.0,
|
|
294
|
+
)
|
|
295
|
+
if providers.status_code == 200:
|
|
296
|
+
console.print("[green]diagnostics/providers[/green]")
|
|
297
|
+
console.print_json(data=providers.json())
|
|
298
|
+
except httpx.HTTPError:
|
|
299
|
+
pass
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
@app.command("agent-context")
|
|
303
|
+
def agent_context_cmd(
|
|
304
|
+
api: str = typer.Option(DEFAULT_API, "--api"),
|
|
305
|
+
output: Path = typer.Option(Path(".hydracept/agent-context.json"), "--output"),
|
|
306
|
+
) -> None:
|
|
307
|
+
response = httpx.get(f"{api.rstrip('/')}/v1/agent-context", timeout=60.0)
|
|
308
|
+
response.raise_for_status()
|
|
309
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
310
|
+
output.write_text(json.dumps(response.json(), indent=2), encoding="utf-8")
|
|
311
|
+
console.print(f"[green]Wrote[/green] {output}")
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@app.command("health")
|
|
315
|
+
def health_cmd(api: str = typer.Option(DEFAULT_API, "--api")) -> None:
|
|
316
|
+
response = httpx.get(f"{api.rstrip('/')}/healthz", timeout=15.0)
|
|
317
|
+
response.raise_for_status()
|
|
318
|
+
console.print_json(data=response.json())
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@app.command("consumer-check")
|
|
322
|
+
def consumer_check_cmd(
|
|
323
|
+
path: Path = typer.Option(Path.cwd(), "--path"),
|
|
324
|
+
strict: bool = typer.Option(False, "--strict"),
|
|
325
|
+
) -> None:
|
|
326
|
+
code, detail = scan_path(path)
|
|
327
|
+
if code != 0:
|
|
328
|
+
console.print(detail)
|
|
329
|
+
raise typer.Exit(1)
|
|
330
|
+
console.print(f"[green]{detail}[/green]")
|
|
331
|
+
if strict:
|
|
332
|
+
console.print("[green]Consumer boundary check passed[/green]")
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
@capabilities_app.command("list")
|
|
336
|
+
def capabilities_list(
|
|
337
|
+
api: str = typer.Option(DEFAULT_API, "--api"),
|
|
338
|
+
) -> None:
|
|
339
|
+
response = httpx.get(f"{api.rstrip('/')}/v1/capabilities", timeout=30.0)
|
|
340
|
+
response.raise_for_status()
|
|
341
|
+
console.print_json(data=response.json())
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
@capabilities_app.command("describe")
|
|
345
|
+
def capabilities_describe(
|
|
346
|
+
key: str = typer.Argument(...),
|
|
347
|
+
api: str = typer.Option(DEFAULT_API, "--api"),
|
|
348
|
+
) -> None:
|
|
349
|
+
response = httpx.get(f"{api.rstrip('/')}/v1/capabilities/{key}", timeout=30.0)
|
|
350
|
+
response.raise_for_status()
|
|
351
|
+
console.print_json(data=response.json())
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
@capabilities_app.command("invoke")
|
|
355
|
+
def capabilities_invoke(
|
|
356
|
+
key: str = typer.Argument(...),
|
|
357
|
+
body: Path = typer.Argument(...),
|
|
358
|
+
api: str = typer.Option(DEFAULT_API, "--api"),
|
|
359
|
+
project_root: Path = typer.Option(Path.cwd(), "--project-root"),
|
|
360
|
+
token: str = typer.Option("", "--token"),
|
|
361
|
+
) -> None:
|
|
362
|
+
resolved = _resolve_token(project_root, token or None)
|
|
363
|
+
payload = json.loads(body.read_text(encoding="utf-8"))
|
|
364
|
+
client = HydraceptClient(api, resolved)
|
|
365
|
+
console.print_json(data=client.invoke_capability(key, payload))
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
@jobs_app.command("submit")
|
|
369
|
+
def jobs_submit(
|
|
370
|
+
capability_key: str = typer.Argument(...),
|
|
371
|
+
body: Path = typer.Argument(...),
|
|
372
|
+
api: str = typer.Option(DEFAULT_API, "--api"),
|
|
373
|
+
project_root: Path = typer.Option(Path.cwd(), "--project-root"),
|
|
374
|
+
token: str = typer.Option("", "--token"),
|
|
375
|
+
watch: bool = typer.Option(False, "--watch"),
|
|
376
|
+
) -> None:
|
|
377
|
+
resolved = _resolve_token(project_root, token or None)
|
|
378
|
+
payload = json.loads(body.read_text(encoding="utf-8"))
|
|
379
|
+
client = HydraceptClient(api, resolved)
|
|
380
|
+
data = client.submit_capability_job(capability_key, payload)
|
|
381
|
+
console.print_json(data=data)
|
|
382
|
+
if not watch:
|
|
383
|
+
return
|
|
384
|
+
job_id = data.get("jobId") or data.get("executionId")
|
|
385
|
+
while job_id:
|
|
386
|
+
status = client.get_job(str(job_id))
|
|
387
|
+
state = status.get("status") or status.get("currentStatus")
|
|
388
|
+
console.print(state)
|
|
389
|
+
if str(state).lower() in {"succeeded", "failed", "canceled", "cancelled"}:
|
|
390
|
+
console.print_json(data=status)
|
|
391
|
+
break
|
|
392
|
+
time.sleep(2)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
@jobs_app.command("get")
|
|
396
|
+
def jobs_get(
|
|
397
|
+
job_id: str = typer.Argument(...),
|
|
398
|
+
api: str = typer.Option(DEFAULT_API, "--api"),
|
|
399
|
+
project_root: Path = typer.Option(Path.cwd(), "--project-root"),
|
|
400
|
+
token: str = typer.Option("", "--token"),
|
|
401
|
+
) -> None:
|
|
402
|
+
client = HydraceptClient(api, _resolve_token(project_root, token or None))
|
|
403
|
+
console.print_json(data=client.get_job(job_id))
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
@jobs_app.command("receipt")
|
|
407
|
+
def jobs_receipt(
|
|
408
|
+
job_id: str = typer.Argument(...),
|
|
409
|
+
api: str = typer.Option(DEFAULT_API, "--api"),
|
|
410
|
+
project_root: Path = typer.Option(Path.cwd(), "--project-root"),
|
|
411
|
+
token: str = typer.Option("", "--token"),
|
|
412
|
+
) -> None:
|
|
413
|
+
client = HydraceptClient(api, _resolve_token(project_root, token or None))
|
|
414
|
+
console.print_json(data=client.get_job_receipt(job_id))
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
@jobs_app.command("cancel")
|
|
418
|
+
def jobs_cancel(
|
|
419
|
+
job_id: str = typer.Argument(...),
|
|
420
|
+
api: str = typer.Option(DEFAULT_API, "--api"),
|
|
421
|
+
project_root: Path = typer.Option(Path.cwd(), "--project-root"),
|
|
422
|
+
token: str = typer.Option("", "--token"),
|
|
423
|
+
) -> None:
|
|
424
|
+
client = HydraceptClient(api, _resolve_token(project_root, token or None))
|
|
425
|
+
console.print_json(data=client.cancel_job(job_id))
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def main() -> None:
|
|
429
|
+
app()
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
if __name__ == "__main__":
|
|
433
|
+
main()
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Embedded consumer-boundary scanner for the public hydracept CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from importlib import resources
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
FORBIDDEN_HOSTS = [
|
|
13
|
+
"api.openai.com",
|
|
14
|
+
"api.elevenlabs.io",
|
|
15
|
+
"api.anthropic.com",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
FORBIDDEN_LEGACY_PATHS = [
|
|
19
|
+
"/v1/visual/jobs",
|
|
20
|
+
"/v1/invocations",
|
|
21
|
+
"POST /v1/jobs",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
FORBIDDEN_PACKAGE_PATTERNS = [
|
|
25
|
+
re.compile(r"ProjectReference.*Zencode Forge", re.I),
|
|
26
|
+
re.compile(r"file:.*Zencode Forge/clients", re.I),
|
|
27
|
+
re.compile(r"file:.*[/\\]Hydracept[/\\](apps|packages|workers)", re.I),
|
|
28
|
+
re.compile(r"from hydracept_api\b", re.I),
|
|
29
|
+
re.compile(r"import hydracept_api\b", re.I),
|
|
30
|
+
re.compile(r"from forge_api\b", re.I),
|
|
31
|
+
re.compile(r"import forge_api\b", re.I),
|
|
32
|
+
re.compile(r"@zencode/forge-client", re.I),
|
|
33
|
+
re.compile(r"Zencode\.Forge\.Client", re.I),
|
|
34
|
+
re.compile(r"zencode-forge", re.I),
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
LEGACY_STALE_PATTERNS = [
|
|
38
|
+
re.compile(r"Zencode Forge", re.I),
|
|
39
|
+
re.compile(r"\bforge consumer-check\b", re.I),
|
|
40
|
+
re.compile(r"\bforge doctor\b", re.I),
|
|
41
|
+
re.compile(r"\bforge login\b", re.I),
|
|
42
|
+
re.compile(r"forge\.zencode", re.I),
|
|
43
|
+
re.compile(r"\bFORGE_[A-Z0-9_]+\b"),
|
|
44
|
+
re.compile(r"forge-dev-token"),
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
SCAN_EXTENSIONS = {".cs", ".ts", ".tsx", ".js", ".mjs", ".py", ".json", ".csproj"}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _load_exceptions() -> list[dict]:
|
|
51
|
+
try:
|
|
52
|
+
data_files = resources.files("hydracept.data")
|
|
53
|
+
text = (data_files / "hydracept-consumer-exceptions.yaml").read_text(encoding="utf-8")
|
|
54
|
+
data = yaml.safe_load(text) or {}
|
|
55
|
+
return list(data.get("exceptions") or [])
|
|
56
|
+
except Exception:
|
|
57
|
+
return []
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _expired_exceptions(exceptions: list[dict]) -> list[str]:
|
|
61
|
+
now = datetime.now(timezone.utc).date()
|
|
62
|
+
expired: list[str] = []
|
|
63
|
+
for entry in exceptions:
|
|
64
|
+
expires = entry.get("expires")
|
|
65
|
+
if not expires:
|
|
66
|
+
continue
|
|
67
|
+
try:
|
|
68
|
+
exp_date = datetime.strptime(str(expires), "%Y-%m-%d").date()
|
|
69
|
+
except ValueError:
|
|
70
|
+
expired.append(str(entry))
|
|
71
|
+
continue
|
|
72
|
+
if exp_date < now:
|
|
73
|
+
expired.append(str(entry))
|
|
74
|
+
return expired
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def scan(root: Path) -> list[str]:
|
|
78
|
+
violations: list[str] = []
|
|
79
|
+
for path in root.rglob("*"):
|
|
80
|
+
if not path.is_file() or path.suffix.lower() not in SCAN_EXTENSIONS:
|
|
81
|
+
continue
|
|
82
|
+
if "node_modules" in path.parts or ".git" in path.parts or "dist" in path.parts:
|
|
83
|
+
continue
|
|
84
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
85
|
+
rel = path.relative_to(root).as_posix()
|
|
86
|
+
for host in FORBIDDEN_HOSTS:
|
|
87
|
+
if host in text:
|
|
88
|
+
violations.append(f"{rel}: direct provider host {host}")
|
|
89
|
+
for legacy_path in FORBIDDEN_LEGACY_PATHS:
|
|
90
|
+
if legacy_path in text:
|
|
91
|
+
violations.append(f"{rel}: legacy API path {legacy_path}")
|
|
92
|
+
for pattern in FORBIDDEN_PACKAGE_PATTERNS:
|
|
93
|
+
if pattern.search(text):
|
|
94
|
+
violations.append(f"{rel}: forbidden package reference ({pattern.pattern})")
|
|
95
|
+
for pattern in LEGACY_STALE_PATTERNS:
|
|
96
|
+
if pattern.search(text):
|
|
97
|
+
violations.append(f"{rel}: legacy Forge branding ({pattern.pattern})")
|
|
98
|
+
return violations
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def scan_path(target: Path) -> tuple[int, str]:
|
|
102
|
+
expired = _expired_exceptions(_load_exceptions())
|
|
103
|
+
if expired:
|
|
104
|
+
return 1, "Expired hydracept-consumer-exceptions:\n" + "\n".join(f" - {e}" for e in expired)
|
|
105
|
+
violations = scan(target)
|
|
106
|
+
if violations:
|
|
107
|
+
detail = "Consumer boundary violations:\n" + "\n".join(f" - {v}" for v in violations)
|
|
108
|
+
return 1, detail
|
|
109
|
+
return 0, f"Consumer boundary OK ({target})"
|
|
File without changes
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hydracept
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client and CLI for the Hydracept public API — AI execution infrastructure for games.
|
|
5
|
+
Author: Zencode
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://hydracept.com
|
|
8
|
+
Project-URL: Documentation, https://hydracept.com/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/hydracept/hydracept-public
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: httpx>=0.27.0
|
|
13
|
+
Requires-Dist: typer>=0.15.0
|
|
14
|
+
Requires-Dist: rich>=13.9.0
|
|
15
|
+
Requires-Dist: PyYAML>=6.0
|
|
16
|
+
|
|
17
|
+
# hydracept
|
|
18
|
+
|
|
19
|
+
Python client for the Hydracept public API — AI execution infrastructure for games.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install hydracept
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from hydracept import HydraceptClient
|
|
27
|
+
|
|
28
|
+
client = HydraceptClient("https://api.hydracept.com", token="...")
|
|
29
|
+
job = client.submit_capability_job(
|
|
30
|
+
"image.generate.v1",
|
|
31
|
+
{
|
|
32
|
+
"context": {
|
|
33
|
+
"productId": "my-product",
|
|
34
|
+
"projectId": "cpr_...",
|
|
35
|
+
"environment": "development",
|
|
36
|
+
},
|
|
37
|
+
"input": {"prompt": "cute slime icon"},
|
|
38
|
+
"execution": {"executionPreference": "automatic"},
|
|
39
|
+
"idempotencyKey": "demo-1",
|
|
40
|
+
},
|
|
41
|
+
)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
CLI: `hydracept login`, `hydracept init`, `hydracept doctor`
|
|
45
|
+
|
|
46
|
+
Docs: https://docs.hydracept.com
|
|
47
|
+
|
|
48
|
+
A Zencode product · © Zencode Consulting Inc.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
hydracept/__init__.py
|
|
4
|
+
hydracept/consumer_boundary.py
|
|
5
|
+
hydracept.egg-info/PKG-INFO
|
|
6
|
+
hydracept.egg-info/SOURCES.txt
|
|
7
|
+
hydracept.egg-info/dependency_links.txt
|
|
8
|
+
hydracept.egg-info/entry_points.txt
|
|
9
|
+
hydracept.egg-info/requires.txt
|
|
10
|
+
hydracept.egg-info/top_level.txt
|
|
11
|
+
hydracept/cli/__init__.py
|
|
12
|
+
hydracept/cli/main.py
|
|
13
|
+
hydracept/data/__init__.py
|
|
14
|
+
hydracept/data/hydracept-consumer-exceptions.yaml
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
hydracept
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "hydracept"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python client and CLI for the Hydracept public API — AI execution infrastructure for games."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Zencode" }]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"httpx>=0.27.0",
|
|
15
|
+
"typer>=0.15.0",
|
|
16
|
+
"rich>=13.9.0",
|
|
17
|
+
"PyYAML>=6.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Homepage = "https://hydracept.com"
|
|
22
|
+
Documentation = "https://hydracept.com/docs"
|
|
23
|
+
Repository = "https://github.com/hydracept/hydracept-public"
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
hydracept = "hydracept.cli.main:app"
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.packages.find]
|
|
29
|
+
where = ["."]
|
|
30
|
+
include = ["hydracept*"]
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.package-data]
|
|
33
|
+
hydracept = ["data/*.yaml"]
|