hyperloop 0.8.1__tar.gz → 0.9.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.
- {hyperloop-0.8.1 → hyperloop-0.9.0}/CHANGELOG.md +3 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/PKG-INFO +1 -1
- {hyperloop-0.8.1 → hyperloop-0.9.0}/pyproject.toml +1 -1
- {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/adapters/matrix_setup.py +68 -24
- {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/config.py +3 -3
- {hyperloop-0.8.1 → hyperloop-0.9.0}/uv.lock +1 -1
- {hyperloop-0.8.1 → hyperloop-0.9.0}/.github/workflows/ci.yaml +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/.github/workflows/release.yaml +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/.gitignore +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/.pre-commit-config.yaml +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/.python-version +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/CLAUDE.md +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/LICENSE +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/README.md +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/base/implementer.yaml +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/base/kustomization.yaml +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/base/pm.yaml +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/base/process-improver.yaml +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/base/process.yaml +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/base/rebase-resolver.yaml +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/base/verifier.yaml +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/specs/observability.md +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/specs/prompts/checklist.md +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/specs/prompts/checks/check_result_file.sh +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/specs/prompts/rules.md +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/specs/spec.md +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/specs/tasks/task-001.md +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/specs/tasks/task-002.md +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/specs/tasks/task-003.md +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/specs/tasks/task-004.md +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/specs/tasks/task-005.md +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/specs/tasks/task-006.md +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/__init__.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/__main__.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/adapters/__init__.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/adapters/git_state.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/adapters/local.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/adapters/matrix_probe.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/adapters/probe.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/adapters/serial.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/adapters/structlog_probe.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/cli.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/compose.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/domain/__init__.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/domain/decide.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/domain/deps.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/domain/model.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/domain/pipeline.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/logging.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/loop.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/ports/__init__.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/ports/pr.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/ports/probe.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/ports/runtime.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/ports/serial.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/ports/state.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/pr.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/__init__.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/fakes/__init__.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/fakes/pr.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/fakes/probe.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/fakes/runtime.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/fakes/serial.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/fakes/state.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/test_cli.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/test_compose.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/test_config.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/test_decide.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/test_deps.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/test_e2e.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/test_fakes.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/test_git_state.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/test_local_runtime.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/test_loop.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/test_matrix_probe.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/test_model.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/test_pipeline.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/test_pr.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/test_probe.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/test_serial_agents.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/test_smoke.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/test_state_contract.py +0 -0
- {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/test_structlog_probe.py +0 -0
|
@@ -182,15 +182,18 @@ def _create_room(
|
|
|
182
182
|
homeserver: str,
|
|
183
183
|
access_token: str,
|
|
184
184
|
room_name: str,
|
|
185
|
+
invite_user: str = "",
|
|
185
186
|
) -> str:
|
|
186
|
-
"""Create a private Matrix room. Returns the room_id."""
|
|
187
|
+
"""Create a private Matrix room and optionally invite a user. Returns the room_id."""
|
|
187
188
|
url = f"{homeserver}/_matrix/client/v3/createRoom"
|
|
188
|
-
body = {
|
|
189
|
+
body: dict[str, object] = {
|
|
189
190
|
"name": room_name,
|
|
190
191
|
"topic": "hyperloop orchestrator notifications",
|
|
191
192
|
"visibility": "private",
|
|
192
193
|
"preset": "private_chat",
|
|
193
194
|
}
|
|
195
|
+
if invite_user:
|
|
196
|
+
body["invite"] = [invite_user]
|
|
194
197
|
|
|
195
198
|
resp = client.post(
|
|
196
199
|
url,
|
|
@@ -213,15 +216,15 @@ def ensure_matrix_ready(
|
|
|
213
216
|
) -> tuple[str, str]:
|
|
214
217
|
"""Ensure Matrix credentials and room are available.
|
|
215
218
|
|
|
216
|
-
|
|
217
|
-
1. Explicit ``token_env`` env var
|
|
218
|
-
2.
|
|
219
|
-
|
|
219
|
+
**Access token** resolution:
|
|
220
|
+
1. Explicit ``token_env`` env var → use directly.
|
|
221
|
+
2. ``registration_token_env`` → register a fresh disposable bot
|
|
222
|
+
(``hyperloop-{repo}-{random}``), deactivate the previous one.
|
|
220
223
|
|
|
221
|
-
|
|
222
|
-
1. Explicit ``room_id`` in config
|
|
223
|
-
2. Cached room_id
|
|
224
|
-
3. Auto-create via Matrix API
|
|
224
|
+
**Room ID** resolution:
|
|
225
|
+
1. Explicit ``room_id`` in config.
|
|
226
|
+
2. Cached room_id from ``.hyperloop/matrix-state.json``.
|
|
227
|
+
3. Auto-create via Matrix API (invites ``invite_user``).
|
|
225
228
|
|
|
226
229
|
Returns:
|
|
227
230
|
(access_token, room_id) tuple. Either or both may be empty string
|
|
@@ -234,17 +237,15 @@ def ensure_matrix_ready(
|
|
|
234
237
|
|
|
235
238
|
# --- Resolve access token ---
|
|
236
239
|
explicit_token = os.environ.get(config.token_env) if config.token_env else ""
|
|
237
|
-
|
|
238
|
-
access_token = explicit_token or cached_token
|
|
240
|
+
access_token = explicit_token
|
|
239
241
|
|
|
240
242
|
if not access_token:
|
|
241
|
-
# Try auto-registration
|
|
242
243
|
registration_token = (
|
|
243
244
|
os.environ.get(config.registration_token_env) if config.registration_token_env else ""
|
|
244
245
|
)
|
|
245
246
|
if registration_token:
|
|
246
247
|
try:
|
|
247
|
-
access_token =
|
|
248
|
+
access_token = _register_disposable_bot(
|
|
248
249
|
config, repo_path, homeserver, registration_token, cache
|
|
249
250
|
)
|
|
250
251
|
except Exception:
|
|
@@ -267,36 +268,53 @@ def ensure_matrix_ready(
|
|
|
267
268
|
_log.exception("Matrix room creation failed")
|
|
268
269
|
return access_token, ""
|
|
269
270
|
|
|
271
|
+
# Ensure the bot has joined the room (it's a new user each run)
|
|
272
|
+
if access_token != explicit_token:
|
|
273
|
+
try:
|
|
274
|
+
_join_room(httpx.Client(timeout=10.0), homeserver, access_token, room_id)
|
|
275
|
+
except Exception:
|
|
276
|
+
_log.exception("Matrix room join failed")
|
|
277
|
+
|
|
270
278
|
return access_token, room_id
|
|
271
279
|
|
|
272
280
|
|
|
273
|
-
def
|
|
281
|
+
def _register_disposable_bot(
|
|
274
282
|
config: MatrixConfig,
|
|
275
283
|
repo_path: Path,
|
|
276
284
|
homeserver: str,
|
|
277
285
|
registration_token: str,
|
|
278
286
|
cache: dict[str, str] | None,
|
|
279
287
|
) -> str:
|
|
280
|
-
"""Register a bot user
|
|
281
|
-
repo_name = repo_path.name
|
|
282
|
-
username = config.bot_username or f"hyperloop-{repo_name}"
|
|
288
|
+
"""Register a fresh disposable bot user. Returns access_token.
|
|
283
289
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
290
|
+
Each run gets a new identity (``hyperloop-{repo}-{random}``). The
|
|
291
|
+
previous bot is deactivated best-effort. Only the room_id is cached.
|
|
292
|
+
"""
|
|
293
|
+
repo_name = repo_path.name
|
|
294
|
+
suffix = secrets.token_hex(4)
|
|
295
|
+
username = f"hyperloop-{repo_name}-{suffix}"
|
|
296
|
+
password = secrets.token_urlsafe(32)
|
|
287
297
|
|
|
288
298
|
client = httpx.Client(timeout=30.0)
|
|
289
299
|
try:
|
|
300
|
+
# Deactivate previous bot (best-effort)
|
|
301
|
+
prev_token = cache.get("access_token", "") if cache else ""
|
|
302
|
+
if prev_token:
|
|
303
|
+
_deactivate_user(client, homeserver, prev_token)
|
|
304
|
+
|
|
305
|
+
# Register new bot
|
|
290
306
|
user_id, access_token = _register_bot(
|
|
291
307
|
client, homeserver, registration_token, username, password
|
|
292
308
|
)
|
|
293
309
|
|
|
310
|
+
# Cache room_id + new bot credentials
|
|
311
|
+
cached_room = cache.get("room_id", "") if cache else ""
|
|
294
312
|
_save_cache(
|
|
295
313
|
repo_path,
|
|
296
314
|
homeserver=homeserver,
|
|
297
315
|
user_id=user_id,
|
|
298
316
|
access_token=access_token,
|
|
299
|
-
room_id=config.room_id,
|
|
317
|
+
room_id=config.room_id or cached_room,
|
|
300
318
|
password=password,
|
|
301
319
|
)
|
|
302
320
|
|
|
@@ -306,17 +324,43 @@ def _auto_register(
|
|
|
306
324
|
client.close()
|
|
307
325
|
|
|
308
326
|
|
|
327
|
+
def _deactivate_user(client: httpx.Client, homeserver: str, access_token: str) -> None:
|
|
328
|
+
"""Deactivate the user associated with the given access token. Best-effort."""
|
|
329
|
+
import contextlib
|
|
330
|
+
|
|
331
|
+
url = f"{homeserver}/_matrix/client/v3/account/deactivate"
|
|
332
|
+
with contextlib.suppress(Exception):
|
|
333
|
+
client.post(
|
|
334
|
+
url,
|
|
335
|
+
json={"auth": {"type": "m.login.password"}},
|
|
336
|
+
headers={"Authorization": f"Bearer {access_token}"},
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _join_room(client: httpx.Client, homeserver: str, access_token: str, room_id: str) -> None:
|
|
341
|
+
"""Join a room by ID."""
|
|
342
|
+
url = f"{homeserver}/_matrix/client/v3/join/{room_id}"
|
|
343
|
+
resp = client.post(url, json={}, headers={"Authorization": f"Bearer {access_token}"})
|
|
344
|
+
resp.raise_for_status()
|
|
345
|
+
|
|
346
|
+
|
|
309
347
|
def _auto_create_room(
|
|
310
348
|
config: MatrixConfig,
|
|
311
349
|
repo_path: Path,
|
|
312
350
|
homeserver: str,
|
|
313
351
|
access_token: str,
|
|
314
352
|
) -> str:
|
|
315
|
-
"""Create a room
|
|
353
|
+
"""Create a room, invite the configured user, and cache room_id. Returns room_id."""
|
|
316
354
|
repo_name = repo_path.name
|
|
317
355
|
client = httpx.Client(timeout=30.0)
|
|
318
356
|
try:
|
|
319
|
-
room_id = _create_room(
|
|
357
|
+
room_id = _create_room(
|
|
358
|
+
client,
|
|
359
|
+
homeserver,
|
|
360
|
+
access_token,
|
|
361
|
+
f"hyperloop-{repo_name}",
|
|
362
|
+
invite_user=config.invite_user,
|
|
363
|
+
)
|
|
320
364
|
|
|
321
365
|
# Update cache with the new room_id
|
|
322
366
|
cache = _load_cache(repo_path, homeserver) or {}
|
|
@@ -31,7 +31,7 @@ class MatrixConfig:
|
|
|
31
31
|
token_env: str
|
|
32
32
|
verbose: bool
|
|
33
33
|
registration_token_env: str # env var holding the registration token
|
|
34
|
-
|
|
34
|
+
invite_user: str # Matrix user ID to invite to auto-created rooms
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
@dataclass(frozen=True)
|
|
@@ -144,7 +144,7 @@ def _flatten_yaml(raw: dict[str, object]) -> dict[str, object]:
|
|
|
144
144
|
flat["matrix_token_env"] = mx.get("token_env", "")
|
|
145
145
|
flat["matrix_verbose"] = mx.get("verbose", False)
|
|
146
146
|
flat["matrix_registration_token_env"] = mx.get("registration_token_env", "")
|
|
147
|
-
flat["
|
|
147
|
+
flat["matrix_invite_user"] = mx.get("invite_user", "")
|
|
148
148
|
|
|
149
149
|
return flat
|
|
150
150
|
|
|
@@ -208,7 +208,7 @@ def load_config(
|
|
|
208
208
|
token_env=token_env,
|
|
209
209
|
verbose=bool(values.get("matrix_verbose", False)),
|
|
210
210
|
registration_token_env=registration_token_env,
|
|
211
|
-
|
|
211
|
+
invite_user=str(values.get("matrix_invite_user", "")),
|
|
212
212
|
)
|
|
213
213
|
|
|
214
214
|
obs_cfg = ObservabilityConfig(
|
|
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
|
|
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
|
|
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
|
|
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
|