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.
@@ -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,4 @@
1
+ # Temporary registry of product-side Hydracept bypasses during consumer migration.
2
+ # Each entry expires; hydracept consumer-check fails on expired entries.
3
+
4
+ exceptions: []
@@ -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,2 @@
1
+ [console_scripts]
2
+ hydracept = hydracept.cli.main:app
@@ -0,0 +1,4 @@
1
+ httpx>=0.27.0
2
+ typer>=0.15.0
3
+ rich>=13.9.0
4
+ PyYAML>=6.0
@@ -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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+