dev-guru 0.2.0__tar.gz → 0.4.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.
Files changed (43) hide show
  1. {dev_guru-0.2.0 → dev_guru-0.4.0}/CHANGELOG.md +51 -0
  2. {dev_guru-0.2.0 → dev_guru-0.4.0}/PKG-INFO +16 -5
  3. {dev_guru-0.2.0 → dev_guru-0.4.0}/README.md +13 -2
  4. {dev_guru-0.2.0 → dev_guru-0.4.0}/pyproject.toml +2 -2
  5. {dev_guru-0.2.0 → dev_guru-0.4.0}/src/dev_guru/cli.py +37 -0
  6. {dev_guru-0.2.0 → dev_guru-0.4.0}/src/dev_guru/client/inprocess.py +53 -0
  7. {dev_guru-0.2.0 → dev_guru-0.4.0}/src/dev_guru/client/negotiation.py +9 -3
  8. {dev_guru-0.2.0 → dev_guru-0.4.0}/src/dev_guru/client/port.py +27 -1
  9. {dev_guru-0.2.0 → dev_guru-0.4.0}/src/dev_guru/errors.py +11 -0
  10. dev_guru-0.4.0/src/dev_guru/gateway/__init__.py +5 -0
  11. dev_guru-0.4.0/src/dev_guru/gateway/server.py +390 -0
  12. dev_guru-0.4.0/src/dev_guru/gateway/static/app.css +73 -0
  13. dev_guru-0.4.0/src/dev_guru/gateway/static/app.js +405 -0
  14. dev_guru-0.4.0/src/dev_guru/gateway/static/favicon.svg +1 -0
  15. dev_guru-0.4.0/src/dev_guru/gateway/static/index.html +37 -0
  16. {dev_guru-0.2.0 → dev_guru-0.4.0}/src/dev_guru/reports/render.py +3 -0
  17. {dev_guru-0.2.0 → dev_guru-0.4.0}/src/dev_guru/version.py +1 -1
  18. dev_guru-0.4.0/tests/test_gateway.py +647 -0
  19. {dev_guru-0.2.0 → dev_guru-0.4.0}/tests/test_negotiation.py +3 -3
  20. {dev_guru-0.2.0 → dev_guru-0.4.0}/.gitignore +0 -0
  21. {dev_guru-0.2.0 → dev_guru-0.4.0}/LICENSE +0 -0
  22. {dev_guru-0.2.0 → dev_guru-0.4.0}/src/dev_guru/__init__.py +0 -0
  23. {dev_guru-0.2.0 → dev_guru-0.4.0}/src/dev_guru/__main__.py +0 -0
  24. {dev_guru-0.2.0 → dev_guru-0.4.0}/src/dev_guru/client/__init__.py +0 -0
  25. {dev_guru-0.2.0 → dev_guru-0.4.0}/src/dev_guru/diagram/__init__.py +0 -0
  26. {dev_guru-0.2.0 → dev_guru-0.4.0}/src/dev_guru/present/__init__.py +0 -0
  27. {dev_guru-0.2.0 → dev_guru-0.4.0}/src/dev_guru/reports/__init__.py +0 -0
  28. {dev_guru-0.2.0 → dev_guru-0.4.0}/src/dev_guru/reports/assets/explorer.css +0 -0
  29. {dev_guru-0.2.0 → dev_guru-0.4.0}/src/dev_guru/reports/assets/explorer.html +0 -0
  30. {dev_guru-0.2.0 → dev_guru-0.4.0}/src/dev_guru/reports/assets/explorer.js +0 -0
  31. {dev_guru-0.2.0 → dev_guru-0.4.0}/src/dev_guru/reports/model.py +0 -0
  32. {dev_guru-0.2.0 → dev_guru-0.4.0}/src/dev_guru/reports/pages.py +0 -0
  33. {dev_guru-0.2.0 → dev_guru-0.4.0}/src/dev_guru/reports/profiles.py +0 -0
  34. {dev_guru-0.2.0 → dev_guru-0.4.0}/src/dev_guru/sdk.py +0 -0
  35. {dev_guru-0.2.0 → dev_guru-0.4.0}/src/dev_guru/service.py +0 -0
  36. {dev_guru-0.2.0 → dev_guru-0.4.0}/tests/conftest.py +0 -0
  37. {dev_guru-0.2.0 → dev_guru-0.4.0}/tests/test_cli.py +0 -0
  38. {dev_guru-0.2.0 → dev_guru-0.4.0}/tests/test_client.py +0 -0
  39. {dev_guru-0.2.0 → dev_guru-0.4.0}/tests/test_diagram.py +0 -0
  40. {dev_guru-0.2.0 → dev_guru-0.4.0}/tests/test_facade_features.py +0 -0
  41. {dev_guru-0.2.0 → dev_guru-0.4.0}/tests/test_packaging.py +0 -0
  42. {dev_guru-0.2.0 → dev_guru-0.4.0}/tests/test_present.py +0 -0
  43. {dev_guru-0.2.0 → dev_guru-0.4.0}/tests/test_reports.py +0 -0
@@ -10,6 +10,57 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) — [SemVer](ht
10
10
 
11
11
  ---
12
12
 
13
+ ## [0.4.0] — 2026-11-03
14
+
15
+ ### Added
16
+
17
+ - **A web interface in `dev serve`.** Overview, runs, a code view and capabilities from your sources, as plain files in
18
+ the package: no Node and no build step. Runs are shown live, survive a tab reload and a lost connection, name any
19
+ events no longer kept, and stop only when you press Cancel. It works from the keyboard and on a phone. Pages that
20
+ don't exist yet say so and name the commands that work today. Guide: [The local gateway](docs/guides/gateway.md).
21
+ - `GET /api/v1/runs`, the runs in a workspace.
22
+ - Real-Chrome journeys for the interface, on a temporary workspace they build themselves (`make web-journeys`, and part
23
+ of the acceptance suite).
24
+ - [ROADMAP.md](ROADMAP.md): what's released and what's planned.
25
+ - Dependabot for `uv` and GitHub Actions, with a seven-day cooldown.
26
+
27
+ ### Changed
28
+
29
+ - Needs Ananke 0.3.19 or newer, for its fixes to path traversal and vulnerable dependencies.
30
+ - CI grants `security-events: write` only to the jobs that upload findings.
31
+ - Releases are created with `gh release` rather than a third-party action.
32
+ - CI installs Trivy with retries and a pinned SHA-256 rather than through a third-party action, so one failed DNS lookup can't block a release.
33
+
34
+ ### Fixed
35
+
36
+ - The run console could stop without showing a run's final event, if it read between the run's final state and that event.
37
+
38
+ - The gateway answered a login token or CSRF token that wasn't ASCII with a `500`. It's now the usual refusal. No request
39
+ got through either way.
40
+
41
+ ### Security
42
+
43
+ - Reviewed two Semgrep findings as false positives and marked each in the code with its reason. Reviewed findings no
44
+ longer reappear as new alerts when a line moves.
45
+
46
+ ---
47
+
48
+ ## [0.3.0] — 2026-11-03
49
+
50
+ ### Added
51
+
52
+ - **`dev serve`, a local gateway to Ananke.** It listens on loopback only. A single-use link signs in one browser, and
53
+ changes also need a same-origin `Origin`, a CSRF token and a JSON body. Workspaces are named by opaque id, and no route
54
+ takes a command or path to run. It starts and cancels runs, reads events and artifacts, previews workflows and serves
55
+ the code graph. There's no web interface yet. Guide: [The local gateway](docs/guides/gateway.md).
56
+ - The client port gains run, event, artifact, plan and code graph operations, so the gateway reaches Ananke only through it.
57
+
58
+ ### Changed
59
+
60
+ - Needs Ananke 0.3.17 or newer, for its facade that's safe across threads.
61
+
62
+ ---
63
+
13
64
  ## [0.2.0] — 2026-11-03
14
65
 
15
66
  ### Added
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: dev-guru
3
- Version: 0.2.0
4
- Summary: Dev Guru: understand, improve, verify and learn from your software. A local-first developer product on the Ananke Plexus harness: Python SDK and CLI first, offline HTML reports, later a local web UI.
3
+ Version: 0.4.0
4
+ Summary: Dev Guru: understand, improve, verify and learn from your software. A local-first developer product on the Ananke Plexus harness: Python SDK and CLI, offline HTML reports and a local web interface.
5
5
  Project-URL: Homepage, https://github.com/Neo-Pragnya/Dev-Guru
6
6
  Project-URL: Repository, https://github.com/Neo-Pragnya/Dev-Guru
7
7
  Project-URL: Issues, https://github.com/Neo-Pragnya/Dev-Guru/issues
@@ -22,7 +22,7 @@ Classifier: Programming Language :: Python :: 3.14
22
22
  Classifier: Topic :: Software Development :: Quality Assurance
23
23
  Classifier: Typing :: Typed
24
24
  Requires-Python: >=3.11
25
- Requires-Dist: ananke-plexus<0.4,>=0.3.13
25
+ Requires-Dist: ananke-plexus<0.4,>=0.3.19
26
26
  Requires-Dist: markdown-it-py<5,>=3
27
27
  Requires-Dist: pydantic<3,>=2.11
28
28
  Requires-Dist: rich>=13
@@ -90,6 +90,7 @@ dev present talk.md # Markdown slides → one offline, accessib
90
90
  dev diagram docs/arch.md # an animated SVG of what a document states, every edge cited
91
91
  dev explore . --profile public # a report safe to publish: no source, docstrings or Git revision
92
92
  dev sources # capabilities from your folders, private Git and Dex Trove
93
+ dev serve # a local web interface: live runs, the code graph, your capabilities
93
94
  dev learning review NAME # review what Ananke learned before it becomes reusable
94
95
  ```
95
96
 
@@ -103,11 +104,20 @@ with GuruClient(workspace="path/to/project") as guru:
103
104
  guru.export(result, output="explorer.html")
104
105
  ```
105
106
 
107
+ ## In your browser
108
+
109
+ `dev serve` opens a web interface to what Ananke computes. It shows what's connected, starts and follows runs (which
110
+ survive a reload or a lost connection, and stop only when you press Cancel), browses the code graph, and searches your
111
+ capabilities. It listens only on your computer, only the link it prints gets in, and it needs no Node: everything comes
112
+ from the [Dev Guru package on PyPI](https://pypi.org/project/dev-guru/). See [The local gateway](docs/guides/gateway.md).
113
+
114
+ What's coming next, including code maps, flows, ERDs and CVE views, is in the [roadmap](ROADMAP.md).
115
+
106
116
  ## How it fits together
107
117
 
108
118
  | Repository | Role |
109
119
  | --- | --- |
110
- | **Dev Guru** (this repo) | The product: CLI and SDK, report view models and renderers, later a local web UI |
120
+ | **Dev Guru** (this repo) | The product: CLI and SDK, report view models and renderers, a local web interface |
111
121
  | [Ananke Plexus](https://github.com/Neo-Pragnya/Ananke-Plexus) | The harness: code graph, analysis, workflows, policy, runs and evidence |
112
122
  | [Dex Trove](https://github.com/Neo-Pragnya/Dex-Trove) | Optional capability memory: curated skills, agents, instructions and workflows |
113
123
 
@@ -118,7 +128,8 @@ required. See [Architecture](docs/concepts/architecture.md) and [How Dev Guru ta
118
128
 
119
129
  ```bash
120
130
  make verify # ruff, ruff format, strict mypy, pytest
121
- make acceptance-wheel # capability scenarios on the built wheel, including real-Chrome report journeys
131
+ make acceptance-wheel # capability scenarios on the built wheel, including real-Chrome journeys
132
+ make web-journeys # just the web interface, in real Chrome, on a temporary workspace
122
133
  make scan # gitleaks, pip-audit, GuardDog and Bandit on dist/
123
134
  ```
124
135
 
@@ -47,6 +47,7 @@ dev present talk.md # Markdown slides → one offline, accessib
47
47
  dev diagram docs/arch.md # an animated SVG of what a document states, every edge cited
48
48
  dev explore . --profile public # a report safe to publish: no source, docstrings or Git revision
49
49
  dev sources # capabilities from your folders, private Git and Dex Trove
50
+ dev serve # a local web interface: live runs, the code graph, your capabilities
50
51
  dev learning review NAME # review what Ananke learned before it becomes reusable
51
52
  ```
52
53
 
@@ -60,11 +61,20 @@ with GuruClient(workspace="path/to/project") as guru:
60
61
  guru.export(result, output="explorer.html")
61
62
  ```
62
63
 
64
+ ## In your browser
65
+
66
+ `dev serve` opens a web interface to what Ananke computes. It shows what's connected, starts and follows runs (which
67
+ survive a reload or a lost connection, and stop only when you press Cancel), browses the code graph, and searches your
68
+ capabilities. It listens only on your computer, only the link it prints gets in, and it needs no Node: everything comes
69
+ from the [Dev Guru package on PyPI](https://pypi.org/project/dev-guru/). See [The local gateway](docs/guides/gateway.md).
70
+
71
+ What's coming next, including code maps, flows, ERDs and CVE views, is in the [roadmap](ROADMAP.md).
72
+
63
73
  ## How it fits together
64
74
 
65
75
  | Repository | Role |
66
76
  | --- | --- |
67
- | **Dev Guru** (this repo) | The product: CLI and SDK, report view models and renderers, later a local web UI |
77
+ | **Dev Guru** (this repo) | The product: CLI and SDK, report view models and renderers, a local web interface |
68
78
  | [Ananke Plexus](https://github.com/Neo-Pragnya/Ananke-Plexus) | The harness: code graph, analysis, workflows, policy, runs and evidence |
69
79
  | [Dex Trove](https://github.com/Neo-Pragnya/Dex-Trove) | Optional capability memory: curated skills, agents, instructions and workflows |
70
80
 
@@ -75,7 +85,8 @@ required. See [Architecture](docs/concepts/architecture.md) and [How Dev Guru ta
75
85
 
76
86
  ```bash
77
87
  make verify # ruff, ruff format, strict mypy, pytest
78
- make acceptance-wheel # capability scenarios on the built wheel, including real-Chrome report journeys
88
+ make acceptance-wheel # capability scenarios on the built wheel, including real-Chrome journeys
89
+ make web-journeys # just the web interface, in real Chrome, on a temporary workspace
79
90
  make scan # gitleaks, pip-audit, GuardDog and Bandit on dist/
80
91
  ```
81
92
 
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
5
5
  [project]
6
6
  name = "dev-guru"
7
7
  dynamic = ["version"]
8
- description = "Dev Guru: understand, improve, verify and learn from your software. A local-first developer product on the Ananke Plexus harness: Python SDK and CLI first, offline HTML reports, later a local web UI."
8
+ description = "Dev Guru: understand, improve, verify and learn from your software. A local-first developer product on the Ananke Plexus harness: Python SDK and CLI, offline HTML reports and a local web interface."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
11
11
  license = "Apache-2.0"
@@ -28,7 +28,7 @@ classifiers = [
28
28
  # Dev Guru is a client of Ananke: the harness owns analysis, execution and evidence (docs/architecture.md).
29
29
  # Keep this range in step with SUPPORTED_ANANKE in src/dev_guru/client/negotiation.py (tests/test_packaging.py).
30
30
  dependencies = [
31
- "ananke-plexus>=0.3.13,<0.4",
31
+ "ananke-plexus>=0.3.19,<0.4",
32
32
  "markdown-it-py>=3,<5",
33
33
  "pydantic>=2.11,<3",
34
34
  "rich>=13",
@@ -286,6 +286,43 @@ def learning_review(
286
286
  webbrowser.open(out.resolve().as_uri())
287
287
 
288
288
 
289
+ @app.command()
290
+ def serve(
291
+ workspaces: Annotated[
292
+ list[Path] | None, typer.Argument(help="Projects the browser may reach (default: here)")
293
+ ] = None,
294
+ port: Annotated[int, typer.Option(help="Local port (default: any free one)")] = 0,
295
+ open_browser: Annotated[bool, typer.Option("--open/--no-open", help="Open the browser")] = True,
296
+ transport: TransportOpt = "in-process",
297
+ ) -> None:
298
+ """Start the local web gateway. It only listens on this computer, and only the printed link gets in."""
299
+ from dev_guru.gateway import Gateway
300
+
301
+ try:
302
+ client = connect(transport)
303
+ client.require_compatible() # type: ignore[attr-defined]
304
+ gateway = Gateway(workspaces or [Path.cwd()], client, port=port)
305
+ except errors.GuruError as exc:
306
+ _fail(exc, False, 2)
307
+ except OSError as exc:
308
+ _fail(
309
+ errors.GuruError(errors.INVALID_INPUT, f"can't listen on port {port}: {exc}"), False, 2
310
+ )
311
+ # printed plainly: rich would wrap a long link, and this link is the way in
312
+ print(
313
+ f"Dev Guru is listening on this computer only. Open this link, which works once:\n{gateway.login_url}",
314
+ flush=True,
315
+ )
316
+ if open_browser:
317
+ webbrowser.open(gateway.login_url)
318
+ try:
319
+ gateway.serve_forever()
320
+ except KeyboardInterrupt:
321
+ pass
322
+ finally:
323
+ gateway.shutdown()
324
+
325
+
289
326
  @app.command()
290
327
  def diagram(
291
328
  document: Annotated[Path, typer.Argument(help="An approved Markdown document")],
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from collections import Counter
6
+ from collections.abc import Callable
6
7
  from pathlib import Path
7
8
  from typing import Any
8
9
 
@@ -87,6 +88,58 @@ class InProcessAnanke:
87
88
  raise errors.GuruError(errors.HARNESS_INCOMPATIBLE, exc.message) from exc
88
89
  return facade
89
90
 
91
+ def _call(self, workspace: Path, work: Callable[[Any], Any]) -> Any:
92
+ """Run one facade call, turning what Ananke refuses into a typed error that keeps its code."""
93
+ from ananke.plexus.app import AppError
94
+
95
+ facade = self._facade(self._root(workspace))
96
+ try:
97
+ return work(facade)
98
+ except AppError as exc:
99
+ raise errors.HarnessRefused(exc.code, exc.message, retryable=exc.retryable) from exc
100
+
101
+ def harness_capabilities(self, workspace: Path) -> dict[str, Any]:
102
+ return dict(self._call(workspace, lambda f: f.capabilities()))
103
+
104
+ def code_view(self, workspace: Path, **filters: Any) -> dict[str, Any]:
105
+ return dict(self._call(workspace, lambda f: f.code_view(**filters)))
106
+
107
+ def prepare_plan(self, workspace: Path, draft: dict[str, Any]) -> dict[str, Any]:
108
+ return dict(self._call(workspace, lambda f: f.preview_workflow(draft)))
109
+
110
+ def submit_run(
111
+ self, workspace: Path, kind: str, parameters: dict[str, Any], idempotency_key: str
112
+ ) -> dict[str, Any]:
113
+ return dict(
114
+ self._call(
115
+ workspace,
116
+ lambda f: f.submit_run(
117
+ f.prepare_run(kind, parameters), idempotency_key, start=False
118
+ ),
119
+ )
120
+ )
121
+
122
+ def drive_run(self, workspace: Path, run_id: str) -> dict[str, Any]:
123
+ return dict(self._call(workspace, lambda f: f.drive(run_id)))
124
+
125
+ def get_run(self, workspace: Path, run_id: str) -> dict[str, Any]:
126
+ return dict(self._call(workspace, lambda f: f.get_run(run_id)))
127
+
128
+ def run_history(self, workspace: Path) -> dict[str, Any]:
129
+ return dict(self._call(workspace, lambda f: f.evidence_view()))
130
+
131
+ def run_events(self, workspace: Path, run_id: str, after: int = 0) -> dict[str, Any]:
132
+ return dict(self._call(workspace, lambda f: f.stream_events(run_id, after)))
133
+
134
+ def cancel_run(self, workspace: Path, run_id: str) -> dict[str, Any]:
135
+ return dict(self._call(workspace, lambda f: f.cancel_run(run_id)))
136
+
137
+ def run_artifacts(self, workspace: Path, run_id: str) -> list[dict[str, Any]]:
138
+ return list(self._call(workspace, lambda f: f.list_artifacts(run_id)))
139
+
140
+ def read_artifact(self, workspace: Path, run_id: str, name: str) -> bytes:
141
+ return bytes(self._call(workspace, lambda f: f.read_artifact(run_id, name)))
142
+
90
143
  def graph_data(self, workspace: Path) -> GraphData:
91
144
  """Run Ananke's graph build through the app facade and read the snapshot artifact it produced."""
92
145
  info = self.require_compatible()
@@ -11,10 +11,8 @@ import importlib
11
11
  import re
12
12
  from importlib import metadata
13
13
 
14
- SUPPORTED_ANANKE = ">=0.3.13,<0.4" # keep in step with pyproject.toml (tests/test_packaging.py)
15
- _MIN = (0, 3, 13)
14
+ SUPPORTED_ANANKE = ">=0.3.19,<0.4" # keep in step with pyproject.toml (tests/test_packaging.py)
16
15
  APP_PROTOCOL = "ananke-app/1" # the app facade protocol this Dev Guru speaks
17
- _BELOW = (0, 4, 0)
18
16
 
19
17
  # Capability id -> (module, attribute path). Required ones are what this release of Dev Guru calls.
20
18
  CAPABILITIES: dict[str, tuple[str, str]] = {
@@ -36,6 +34,12 @@ def parse_version(text: str) -> tuple[int, int, int] | None:
36
34
  return (int(m.group(1)), int(m.group(2)), int(m.group(3))) if m else None
37
35
 
38
36
 
37
+ _MIN, _BELOW = (
38
+ tuple(int(part) for part in bound.split("."))
39
+ for bound in re.fullmatch(r">=([\d.]+),<([\d.]+)", SUPPORTED_ANANKE).groups() # type: ignore[union-attr]
40
+ )
41
+
42
+
39
43
  def supported(version: str) -> bool:
40
44
  parsed = parse_version(version)
41
45
  return parsed is not None and _MIN <= parsed < _BELOW
@@ -53,6 +57,8 @@ def probe(capabilities: dict[str, tuple[str, str]] = CAPABILITIES) -> dict[str,
53
57
  found: dict[str, bool] = {}
54
58
  for cap, (module, attr_path) in capabilities.items():
55
59
  try:
60
+ # False positive: module names come from the fixed CAPABILITIES table above, never from a request or a file.
61
+ # nosemgrep: python.lang.security.audit.non-literal-import.non-literal-import
56
62
  obj: object = importlib.import_module(module)
57
63
  for part in attr_path.split("."):
58
64
  obj = getattr(obj, part)
@@ -9,7 +9,7 @@ silently swapped for another (docs/concepts/ananke-client.md).
9
9
  from __future__ import annotations
10
10
 
11
11
  from pathlib import Path
12
- from typing import Protocol, runtime_checkable
12
+ from typing import Any, Protocol, runtime_checkable
13
13
 
14
14
  from pydantic import BaseModel, Field
15
15
 
@@ -125,6 +125,32 @@ class AnankeClient(Protocol):
125
125
 
126
126
  def learning_candidate(self, workspace: Path, name: str) -> LearningCandidate: ...
127
127
 
128
+ def harness_capabilities(self, workspace: Path) -> dict[str, Any]: ...
129
+
130
+ def code_view(self, workspace: Path, **filters: Any) -> dict[str, Any]: ...
131
+
132
+ def prepare_plan(self, workspace: Path, draft: dict[str, Any]) -> dict[str, Any]: ...
133
+
134
+ def submit_run(
135
+ self, workspace: Path, kind: str, parameters: dict[str, Any], idempotency_key: str
136
+ ) -> dict[str, Any]:
137
+ """Record a run without starting it. The same key and request return the same run."""
138
+ ...
139
+
140
+ def drive_run(self, workspace: Path, run_id: str) -> dict[str, Any]: ...
141
+
142
+ def get_run(self, workspace: Path, run_id: str) -> dict[str, Any]: ...
143
+
144
+ def run_history(self, workspace: Path) -> dict[str, Any]: ...
145
+
146
+ def run_events(self, workspace: Path, run_id: str, after: int = 0) -> dict[str, Any]: ...
147
+
148
+ def cancel_run(self, workspace: Path, run_id: str) -> dict[str, Any]: ...
149
+
150
+ def run_artifacts(self, workspace: Path, run_id: str) -> list[dict[str, Any]]: ...
151
+
152
+ def read_artifact(self, workspace: Path, run_id: str, name: str) -> bytes: ...
153
+
128
154
 
129
155
  __all__ = [
130
156
  "AnankeClient",
@@ -10,6 +10,7 @@ UNSUPPORTED_TRANSPORT = "UNSUPPORTED_TRANSPORT"
10
10
  WORKSPACE_INVALID = "WORKSPACE_INVALID"
11
11
  HARNESS_FAILED = "HARNESS_FAILED"
12
12
  INVALID_INPUT = "INVALID_INPUT"
13
+ HARNESS_REFUSED = "HARNESS_REFUSED"
13
14
  CODES = frozenset(
14
15
  {
15
16
  HARNESS_MISSING,
@@ -19,6 +20,7 @@ CODES = frozenset(
19
20
  WORKSPACE_INVALID,
20
21
  HARNESS_FAILED,
21
22
  INVALID_INPUT,
23
+ HARNESS_REFUSED,
22
24
  }
23
25
  )
24
26
 
@@ -44,3 +46,12 @@ class GuruError(Exception):
44
46
  "hint": self.hint,
45
47
  "run_id": self.run_id,
46
48
  }
49
+
50
+
51
+ class HarnessRefused(GuruError):
52
+ """Ananke refused a request. ``harness_code`` is its own stable code, kept so a client can branch on it."""
53
+
54
+ def __init__(self, harness_code: str, message: str, *, retryable: bool = False) -> None:
55
+ super().__init__(HARNESS_REFUSED, message)
56
+ self.harness_code = harness_code
57
+ self.retryable = retryable
@@ -0,0 +1,5 @@
1
+ """The local web gateway: authenticates one browser session and forwards to the Ananke client port."""
2
+
3
+ from dev_guru.gateway.server import Gateway
4
+
5
+ __all__ = ["Gateway"]