hyperloop 0.9.0__tar.gz → 0.9.1__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 (83) hide show
  1. {hyperloop-0.9.0 → hyperloop-0.9.1}/CHANGELOG.md +8 -0
  2. {hyperloop-0.9.0 → hyperloop-0.9.1}/PKG-INFO +1 -1
  3. {hyperloop-0.9.0 → hyperloop-0.9.1}/pyproject.toml +1 -1
  4. {hyperloop-0.9.0 → hyperloop-0.9.1}/src/hyperloop/adapters/matrix_setup.py +41 -40
  5. {hyperloop-0.9.0 → hyperloop-0.9.1}/uv.lock +1 -1
  6. {hyperloop-0.9.0 → hyperloop-0.9.1}/.github/workflows/ci.yaml +0 -0
  7. {hyperloop-0.9.0 → hyperloop-0.9.1}/.github/workflows/release.yaml +0 -0
  8. {hyperloop-0.9.0 → hyperloop-0.9.1}/.gitignore +0 -0
  9. {hyperloop-0.9.0 → hyperloop-0.9.1}/.pre-commit-config.yaml +0 -0
  10. {hyperloop-0.9.0 → hyperloop-0.9.1}/.python-version +0 -0
  11. {hyperloop-0.9.0 → hyperloop-0.9.1}/CLAUDE.md +0 -0
  12. {hyperloop-0.9.0 → hyperloop-0.9.1}/LICENSE +0 -0
  13. {hyperloop-0.9.0 → hyperloop-0.9.1}/README.md +0 -0
  14. {hyperloop-0.9.0 → hyperloop-0.9.1}/base/implementer.yaml +0 -0
  15. {hyperloop-0.9.0 → hyperloop-0.9.1}/base/kustomization.yaml +0 -0
  16. {hyperloop-0.9.0 → hyperloop-0.9.1}/base/pm.yaml +0 -0
  17. {hyperloop-0.9.0 → hyperloop-0.9.1}/base/process-improver.yaml +0 -0
  18. {hyperloop-0.9.0 → hyperloop-0.9.1}/base/process.yaml +0 -0
  19. {hyperloop-0.9.0 → hyperloop-0.9.1}/base/rebase-resolver.yaml +0 -0
  20. {hyperloop-0.9.0 → hyperloop-0.9.1}/base/verifier.yaml +0 -0
  21. {hyperloop-0.9.0 → hyperloop-0.9.1}/specs/observability.md +0 -0
  22. {hyperloop-0.9.0 → hyperloop-0.9.1}/specs/prompts/checklist.md +0 -0
  23. {hyperloop-0.9.0 → hyperloop-0.9.1}/specs/prompts/checks/check_result_file.sh +0 -0
  24. {hyperloop-0.9.0 → hyperloop-0.9.1}/specs/prompts/rules.md +0 -0
  25. {hyperloop-0.9.0 → hyperloop-0.9.1}/specs/spec.md +0 -0
  26. {hyperloop-0.9.0 → hyperloop-0.9.1}/specs/tasks/task-001.md +0 -0
  27. {hyperloop-0.9.0 → hyperloop-0.9.1}/specs/tasks/task-002.md +0 -0
  28. {hyperloop-0.9.0 → hyperloop-0.9.1}/specs/tasks/task-003.md +0 -0
  29. {hyperloop-0.9.0 → hyperloop-0.9.1}/specs/tasks/task-004.md +0 -0
  30. {hyperloop-0.9.0 → hyperloop-0.9.1}/specs/tasks/task-005.md +0 -0
  31. {hyperloop-0.9.0 → hyperloop-0.9.1}/specs/tasks/task-006.md +0 -0
  32. {hyperloop-0.9.0 → hyperloop-0.9.1}/src/hyperloop/__init__.py +0 -0
  33. {hyperloop-0.9.0 → hyperloop-0.9.1}/src/hyperloop/__main__.py +0 -0
  34. {hyperloop-0.9.0 → hyperloop-0.9.1}/src/hyperloop/adapters/__init__.py +0 -0
  35. {hyperloop-0.9.0 → hyperloop-0.9.1}/src/hyperloop/adapters/git_state.py +0 -0
  36. {hyperloop-0.9.0 → hyperloop-0.9.1}/src/hyperloop/adapters/local.py +0 -0
  37. {hyperloop-0.9.0 → hyperloop-0.9.1}/src/hyperloop/adapters/matrix_probe.py +0 -0
  38. {hyperloop-0.9.0 → hyperloop-0.9.1}/src/hyperloop/adapters/probe.py +0 -0
  39. {hyperloop-0.9.0 → hyperloop-0.9.1}/src/hyperloop/adapters/serial.py +0 -0
  40. {hyperloop-0.9.0 → hyperloop-0.9.1}/src/hyperloop/adapters/structlog_probe.py +0 -0
  41. {hyperloop-0.9.0 → hyperloop-0.9.1}/src/hyperloop/cli.py +0 -0
  42. {hyperloop-0.9.0 → hyperloop-0.9.1}/src/hyperloop/compose.py +0 -0
  43. {hyperloop-0.9.0 → hyperloop-0.9.1}/src/hyperloop/config.py +0 -0
  44. {hyperloop-0.9.0 → hyperloop-0.9.1}/src/hyperloop/domain/__init__.py +0 -0
  45. {hyperloop-0.9.0 → hyperloop-0.9.1}/src/hyperloop/domain/decide.py +0 -0
  46. {hyperloop-0.9.0 → hyperloop-0.9.1}/src/hyperloop/domain/deps.py +0 -0
  47. {hyperloop-0.9.0 → hyperloop-0.9.1}/src/hyperloop/domain/model.py +0 -0
  48. {hyperloop-0.9.0 → hyperloop-0.9.1}/src/hyperloop/domain/pipeline.py +0 -0
  49. {hyperloop-0.9.0 → hyperloop-0.9.1}/src/hyperloop/logging.py +0 -0
  50. {hyperloop-0.9.0 → hyperloop-0.9.1}/src/hyperloop/loop.py +0 -0
  51. {hyperloop-0.9.0 → hyperloop-0.9.1}/src/hyperloop/ports/__init__.py +0 -0
  52. {hyperloop-0.9.0 → hyperloop-0.9.1}/src/hyperloop/ports/pr.py +0 -0
  53. {hyperloop-0.9.0 → hyperloop-0.9.1}/src/hyperloop/ports/probe.py +0 -0
  54. {hyperloop-0.9.0 → hyperloop-0.9.1}/src/hyperloop/ports/runtime.py +0 -0
  55. {hyperloop-0.9.0 → hyperloop-0.9.1}/src/hyperloop/ports/serial.py +0 -0
  56. {hyperloop-0.9.0 → hyperloop-0.9.1}/src/hyperloop/ports/state.py +0 -0
  57. {hyperloop-0.9.0 → hyperloop-0.9.1}/src/hyperloop/pr.py +0 -0
  58. {hyperloop-0.9.0 → hyperloop-0.9.1}/tests/__init__.py +0 -0
  59. {hyperloop-0.9.0 → hyperloop-0.9.1}/tests/fakes/__init__.py +0 -0
  60. {hyperloop-0.9.0 → hyperloop-0.9.1}/tests/fakes/pr.py +0 -0
  61. {hyperloop-0.9.0 → hyperloop-0.9.1}/tests/fakes/probe.py +0 -0
  62. {hyperloop-0.9.0 → hyperloop-0.9.1}/tests/fakes/runtime.py +0 -0
  63. {hyperloop-0.9.0 → hyperloop-0.9.1}/tests/fakes/serial.py +0 -0
  64. {hyperloop-0.9.0 → hyperloop-0.9.1}/tests/fakes/state.py +0 -0
  65. {hyperloop-0.9.0 → hyperloop-0.9.1}/tests/test_cli.py +0 -0
  66. {hyperloop-0.9.0 → hyperloop-0.9.1}/tests/test_compose.py +0 -0
  67. {hyperloop-0.9.0 → hyperloop-0.9.1}/tests/test_config.py +0 -0
  68. {hyperloop-0.9.0 → hyperloop-0.9.1}/tests/test_decide.py +0 -0
  69. {hyperloop-0.9.0 → hyperloop-0.9.1}/tests/test_deps.py +0 -0
  70. {hyperloop-0.9.0 → hyperloop-0.9.1}/tests/test_e2e.py +0 -0
  71. {hyperloop-0.9.0 → hyperloop-0.9.1}/tests/test_fakes.py +0 -0
  72. {hyperloop-0.9.0 → hyperloop-0.9.1}/tests/test_git_state.py +0 -0
  73. {hyperloop-0.9.0 → hyperloop-0.9.1}/tests/test_local_runtime.py +0 -0
  74. {hyperloop-0.9.0 → hyperloop-0.9.1}/tests/test_loop.py +0 -0
  75. {hyperloop-0.9.0 → hyperloop-0.9.1}/tests/test_matrix_probe.py +0 -0
  76. {hyperloop-0.9.0 → hyperloop-0.9.1}/tests/test_model.py +0 -0
  77. {hyperloop-0.9.0 → hyperloop-0.9.1}/tests/test_pipeline.py +0 -0
  78. {hyperloop-0.9.0 → hyperloop-0.9.1}/tests/test_pr.py +0 -0
  79. {hyperloop-0.9.0 → hyperloop-0.9.1}/tests/test_probe.py +0 -0
  80. {hyperloop-0.9.0 → hyperloop-0.9.1}/tests/test_serial_agents.py +0 -0
  81. {hyperloop-0.9.0 → hyperloop-0.9.1}/tests/test_smoke.py +0 -0
  82. {hyperloop-0.9.0 → hyperloop-0.9.1}/tests/test_state_contract.py +0 -0
  83. {hyperloop-0.9.0 → hyperloop-0.9.1}/tests/test_structlog_probe.py +0 -0
@@ -2,6 +2,14 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v0.9.1 (2026-04-15)
6
+
7
+ ### Bug Fixes
8
+
9
+ - **matrix**: Proper UIA two-step registration flow
10
+ ([`97080f9`](https://github.com/jsell-rh/hyperloop/commit/97080f9d6980e914451a59d4b2453c7b5294ed30))
11
+
12
+
5
13
  ## v0.9.0 (2026-04-15)
6
14
 
7
15
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperloop
3
- Version: 0.9.0
3
+ Version: 0.9.1
4
4
  Summary: Orchestrator that walks tasks through composable process pipelines using AI agents
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hyperloop"
3
- version = "0.9.0"
3
+ version = "0.9.1"
4
4
  description = "Orchestrator that walks tasks through composable process pipelines using AI agents"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -11,18 +11,18 @@ auto-setup and cache.
11
11
  from __future__ import annotations
12
12
 
13
13
  import json
14
- import logging
15
14
  import secrets
16
15
  from typing import TYPE_CHECKING, cast
17
16
 
18
17
  import httpx
18
+ import structlog
19
19
 
20
20
  if TYPE_CHECKING:
21
21
  from pathlib import Path
22
22
 
23
23
  from hyperloop.config import MatrixConfig
24
24
 
25
- _log = logging.getLogger(__name__)
25
+ _log: structlog.stdlib.BoundLogger = structlog.get_logger()
26
26
 
27
27
  _CACHE_FILE = ".hyperloop/matrix-state.json"
28
28
 
@@ -53,6 +53,8 @@ def _ensure_gitignored(repo_path: Path) -> None:
53
53
  """Ensure .hyperloop/ is in the target repo's .gitignore."""
54
54
  gitignore = repo_path / ".gitignore"
55
55
  entry = ".hyperloop/"
56
+ if not repo_path.is_dir():
57
+ return
56
58
  if gitignore.is_file():
57
59
  content = gitignore.read_text()
58
60
  if entry in content.splitlines():
@@ -104,55 +106,52 @@ def _register_bot(
104
106
  username: str,
105
107
  password: str,
106
108
  ) -> tuple[str, str]:
107
- """Register a bot user. Returns (user_id, access_token).
109
+ """Register a bot user via UIA flow. Returns (user_id, access_token).
108
110
 
109
- Uses Synapse's shared-secret registration flow. If the user already
110
- exists (HTTP 400), falls back to login with the given password.
111
+ Matrix UIA (User-Interactive Authentication) registration:
112
+ 1. POST /register without auth server returns 401 with session + flows.
113
+ 2. POST /register with auth (type + token + session) → 200 with credentials.
114
+
115
+ If the user already exists (HTTP 400), falls back to login.
111
116
  """
112
117
  url = f"{homeserver}/_matrix/client/v3/register"
113
- body = {
118
+ base_body: dict[str, object] = {
114
119
  "username": username,
115
120
  "password": password,
116
- "auth": {
117
- "type": "m.login.registration_token",
118
- "token": registration_token,
119
- },
120
121
  "initial_device_display_name": "hyperloop",
121
122
  "inhibit_login": False,
122
123
  }
123
124
 
124
- resp = client.post(url, json=body)
125
+ # Step 1: request without auth to get session
126
+ resp = client.post(url, json=base_body)
125
127
 
126
128
  if resp.status_code == 200:
129
+ # Some servers accept registration without UIA
127
130
  data = resp.json()
128
131
  return str(data["user_id"]), str(data["access_token"])
129
132
 
130
- if resp.status_code == 401:
131
- # Server requires a different auth flow — try the flows it offers
132
- data = resp.json()
133
- flows = data.get("flows", [])
134
- session = data.get("session", "")
135
-
136
- # Check if m.login.registration_token is among the available flows
137
- for flow in flows:
138
- stages = flow.get("stages", [])
139
- if "m.login.registration_token" in stages:
140
- body["auth"] = {
141
- "type": "m.login.registration_token",
142
- "token": registration_token,
143
- "session": session,
144
- }
145
- resp = client.post(url, json=body)
146
- if resp.status_code == 200:
147
- data = resp.json()
148
- return str(data["user_id"]), str(data["access_token"])
149
-
150
133
  if resp.status_code == 400:
151
- # User likely already exists — try login
152
134
  return _login(client, homeserver, username, password)
153
135
 
136
+ # Step 2: server should return 401 with session + flows
137
+ if resp.status_code in (401, 403):
138
+ data = resp.json()
139
+ session = data.get("session", "")
140
+ if session:
141
+ base_body["auth"] = {
142
+ "type": "m.login.registration_token",
143
+ "token": registration_token,
144
+ "session": session,
145
+ }
146
+ resp = client.post(url, json=base_body)
147
+ if resp.status_code == 200:
148
+ data = resp.json()
149
+ return str(data["user_id"]), str(data["access_token"])
150
+ if resp.status_code == 400:
151
+ return _login(client, homeserver, username, password)
152
+
154
153
  resp.raise_for_status()
155
- msg = f"Unexpected registration response: {resp.status_code}"
154
+ msg = f"Registration failed: {resp.status_code} {resp.text}"
156
155
  raise RuntimeError(msg)
157
156
 
158
157
 
@@ -249,9 +248,9 @@ def ensure_matrix_ready(
249
248
  config, repo_path, homeserver, registration_token, cache
250
249
  )
251
250
  except Exception:
252
- _log.exception("Matrix bot registration failed")
251
+ _log.exception("matrix_bot_registration_failed")
253
252
  else:
254
- _log.warning("Matrix: no access token and no registration token — skipping")
253
+ _log.warning("matrix_skipped", reason="no access token and no registration token")
255
254
  return "", ""
256
255
 
257
256
  if not access_token:
@@ -262,18 +261,20 @@ def ensure_matrix_ready(
262
261
  room_id = config.room_id or cached_room
263
262
 
264
263
  if not room_id:
264
+ _log.info("matrix_room_creating")
265
265
  try:
266
266
  room_id = _auto_create_room(config, repo_path, homeserver, access_token)
267
267
  except Exception:
268
- _log.exception("Matrix room creation failed")
268
+ _log.exception("matrix_room_creation_failed")
269
269
  return access_token, ""
270
270
 
271
271
  # Ensure the bot has joined the room (it's a new user each run)
272
- if access_token != explicit_token:
272
+ if access_token != explicit_token and room_id:
273
+ _log.info("matrix_room_joining", room_id=room_id)
273
274
  try:
274
275
  _join_room(httpx.Client(timeout=10.0), homeserver, access_token, room_id)
275
276
  except Exception:
276
- _log.exception("Matrix room join failed")
277
+ _log.exception("matrix_room_join_failed", room_id=room_id)
277
278
 
278
279
  return access_token, room_id
279
280
 
@@ -318,7 +319,7 @@ def _register_disposable_bot(
318
319
  password=password,
319
320
  )
320
321
 
321
- _log.info("Matrix bot registered: %s", user_id)
322
+ _log.info("matrix_bot_registered", user_id=user_id)
322
323
  return access_token
323
324
  finally:
324
325
  client.close()
@@ -373,7 +374,7 @@ def _auto_create_room(
373
374
  password=cache.get("password", ""),
374
375
  )
375
376
 
376
- _log.info("Matrix room created: %s", room_id)
377
+ _log.info("matrix_room_created", room_id=room_id)
377
378
  return room_id
378
379
  finally:
379
380
  client.close()
@@ -120,7 +120,7 @@ wheels = [
120
120
 
121
121
  [[package]]
122
122
  name = "hyperloop"
123
- version = "0.8.1"
123
+ version = "0.9.0"
124
124
  source = { editable = "." }
125
125
  dependencies = [
126
126
  { name = "httpx" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes