alissa-tools-github-revloop 0.31.11__tar.gz → 0.31.13__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.
- {alissa_tools_github_revloop-0.31.11/src/main/alissa_tools_github_revloop.egg-info → alissa_tools_github_revloop-0.31.13}/PKG-INFO +1 -1
- {alissa_tools_github_revloop-0.31.11 → alissa_tools_github_revloop-0.31.13}/src/main/alissa/tools/github/revloop/fleet_vitals.py +175 -27
- {alissa_tools_github_revloop-0.31.11 → alissa_tools_github_revloop-0.31.13}/src/main/alissa/tools/github/revloop/loop.py +16 -1
- alissa_tools_github_revloop-0.31.13/src/main/alissa/tools/github/revloop/version +1 -0
- {alissa_tools_github_revloop-0.31.11 → alissa_tools_github_revloop-0.31.13}/src/main/alissa/tools/github/revloop/webui/__main__.py +4 -3
- {alissa_tools_github_revloop-0.31.11 → alissa_tools_github_revloop-0.31.13}/src/main/alissa/tools/github/revloop/webui/server.py +28 -1
- {alissa_tools_github_revloop-0.31.11 → alissa_tools_github_revloop-0.31.13/src/main/alissa_tools_github_revloop.egg-info}/PKG-INFO +1 -1
- alissa_tools_github_revloop-0.31.11/src/main/alissa/tools/github/revloop/version +0 -1
- {alissa_tools_github_revloop-0.31.11 → alissa_tools_github_revloop-0.31.13}/LICENSE +0 -0
- {alissa_tools_github_revloop-0.31.11 → alissa_tools_github_revloop-0.31.13}/MANIFEST.in +0 -0
- {alissa_tools_github_revloop-0.31.11 → alissa_tools_github_revloop-0.31.13}/NOTICE +0 -0
- {alissa_tools_github_revloop-0.31.11 → alissa_tools_github_revloop-0.31.13}/README.md +0 -0
- {alissa_tools_github_revloop-0.31.11 → alissa_tools_github_revloop-0.31.13}/requirements.txt +0 -0
- {alissa_tools_github_revloop-0.31.11 → alissa_tools_github_revloop-0.31.13}/setup.cfg +0 -0
- {alissa_tools_github_revloop-0.31.11 → alissa_tools_github_revloop-0.31.13}/setup.py +0 -0
- {alissa_tools_github_revloop-0.31.11 → alissa_tools_github_revloop-0.31.13}/src/main/alissa/tools/github/revloop/__init__.py +0 -0
- {alissa_tools_github_revloop-0.31.11 → alissa_tools_github_revloop-0.31.13}/src/main/alissa/tools/github/revloop/__main__.py +0 -0
- {alissa_tools_github_revloop-0.31.11 → alissa_tools_github_revloop-0.31.13}/src/main/alissa/tools/github/revloop/alissa.py +0 -0
- {alissa_tools_github_revloop-0.31.11 → alissa_tools_github_revloop-0.31.13}/src/main/alissa/tools/github/revloop/alissa_client.py +0 -0
- {alissa_tools_github_revloop-0.31.11 → alissa_tools_github_revloop-0.31.13}/src/main/alissa/tools/github/revloop/bows.py +0 -0
- {alissa_tools_github_revloop-0.31.11 → alissa_tools_github_revloop-0.31.13}/src/main/alissa/tools/github/revloop/config.py +0 -0
- {alissa_tools_github_revloop-0.31.11 → alissa_tools_github_revloop-0.31.13}/src/main/alissa/tools/github/revloop/ghclient.py +0 -0
- {alissa_tools_github_revloop-0.31.11 → alissa_tools_github_revloop-0.31.13}/src/main/alissa/tools/github/revloop/loop_events.py +0 -0
- {alissa_tools_github_revloop-0.31.11 → alissa_tools_github_revloop-0.31.13}/src/main/alissa/tools/github/revloop/parking.py +0 -0
- {alissa_tools_github_revloop-0.31.11 → alissa_tools_github_revloop-0.31.13}/src/main/alissa/tools/github/revloop/proc.py +0 -0
- {alissa_tools_github_revloop-0.31.11 → alissa_tools_github_revloop-0.31.13}/src/main/alissa/tools/github/revloop/prompts.py +0 -0
- {alissa_tools_github_revloop-0.31.11 → alissa_tools_github_revloop-0.31.13}/src/main/alissa/tools/github/revloop/prreview.py +0 -0
- {alissa_tools_github_revloop-0.31.11 → alissa_tools_github_revloop-0.31.13}/src/main/alissa/tools/github/revloop/state.py +0 -0
- {alissa_tools_github_revloop-0.31.11 → alissa_tools_github_revloop-0.31.13}/src/main/alissa/tools/github/revloop/trust.py +0 -0
- {alissa_tools_github_revloop-0.31.11 → alissa_tools_github_revloop-0.31.13}/src/main/alissa/tools/github/revloop/version.py +0 -0
- {alissa_tools_github_revloop-0.31.11 → alissa_tools_github_revloop-0.31.13}/src/main/alissa/tools/github/revloop/webui/__init__.py +0 -0
- {alissa_tools_github_revloop-0.31.11 → alissa_tools_github_revloop-0.31.13}/src/main/alissa/tools/github/revloop/webui/auth.py +0 -0
- {alissa_tools_github_revloop-0.31.11 → alissa_tools_github_revloop-0.31.13}/src/main/alissa/tools/github/revloop/webui/page.py +0 -0
- {alissa_tools_github_revloop-0.31.11 → alissa_tools_github_revloop-0.31.13}/src/main/alissa/tools/github/revloop/webui/sources.py +0 -0
- {alissa_tools_github_revloop-0.31.11 → alissa_tools_github_revloop-0.31.13}/src/main/alissa/tools/github/revloop/webui/sysinfo.py +0 -0
- {alissa_tools_github_revloop-0.31.11 → alissa_tools_github_revloop-0.31.13}/src/main/alissa_tools_github_revloop.egg-info/SOURCES.txt +0 -0
- {alissa_tools_github_revloop-0.31.11 → alissa_tools_github_revloop-0.31.13}/src/main/alissa_tools_github_revloop.egg-info/dependency_links.txt +0 -0
- {alissa_tools_github_revloop-0.31.11 → alissa_tools_github_revloop-0.31.13}/src/main/alissa_tools_github_revloop.egg-info/entry_points.txt +0 -0
- {alissa_tools_github_revloop-0.31.11 → alissa_tools_github_revloop-0.31.13}/src/main/alissa_tools_github_revloop.egg-info/top_level.txt +0 -0
|
@@ -50,15 +50,32 @@ answers (`FleetVitalsPusher.push_now`). Until Studio's schema carries the
|
|
|
50
50
|
block (the design's lane L3), a strict 400 naming `idle` makes the pusher
|
|
51
51
|
resend without it and leave it out for IDLE_REPROBE_S: the vitals the Factory
|
|
52
52
|
already renders never go dark over a field nobody reads yet.
|
|
53
|
+
|
|
54
|
+
The managed fleet's smoke (issue #159, studio.alissa.app provisioner design
|
|
55
|
+
§2.7, lane L9) adds the `config` block -- what this seat DERIVED, in its own
|
|
56
|
+
words: the feed owner actor id, the effective review allowlist, the reviewer
|
|
57
|
+
login(s) it requests (none: the reviewer seat is the one requested) and its
|
|
58
|
+
own GitHub login, the reviewer identity SM4 compares with the rev seat
|
|
59
|
+
credential. Every value is an identity or a name, never a credential: the
|
|
60
|
+
block is built from the config the watcher RUNS on, not from the
|
|
61
|
+
environment, and any value carrying a secret the process holds
|
|
62
|
+
(`SECRET_ENV`, plus the variable `reviewer_token_env` names) is blanked
|
|
63
|
+
before it can leave. The daemon's own pass sends it; the console's
|
|
64
|
+
out-of-pass push leaves it out (that process never resolves the feed
|
|
65
|
+
authority nor refreshes the allowlist, so its answer would be a different
|
|
66
|
+
one). A Studio whose strict schema predates the block gets the `idle`
|
|
67
|
+
treatment: resent without it, offered again after BLOCK_REPROBE_S.
|
|
53
68
|
"""
|
|
54
69
|
|
|
55
70
|
from __future__ import annotations
|
|
56
71
|
|
|
57
72
|
import json
|
|
58
73
|
import logging
|
|
74
|
+
import os
|
|
75
|
+
import re
|
|
59
76
|
import time
|
|
60
77
|
from datetime import datetime, timezone
|
|
61
|
-
from typing import TYPE_CHECKING, Any, Callable
|
|
78
|
+
from typing import TYPE_CHECKING, Any, Callable, Mapping, Sequence
|
|
62
79
|
|
|
63
80
|
from . import parking
|
|
64
81
|
from .alissa import parse_session_name, session_repo_slug
|
|
@@ -68,7 +85,7 @@ from .alissa_client import (
|
|
|
68
85
|
AlissaClient,
|
|
69
86
|
AlissaError,
|
|
70
87
|
)
|
|
71
|
-
from .config import Config
|
|
88
|
+
from .config import REPOS_BOWS, Config
|
|
72
89
|
from .proc import CommandError, run as proc_run
|
|
73
90
|
|
|
74
91
|
if TYPE_CHECKING: # pragma: no cover - typing only
|
|
@@ -97,11 +114,31 @@ VITALS_PUSHED = "pushed"
|
|
|
97
114
|
VITALS_SKIPPED = "skipped"
|
|
98
115
|
VITALS_FAILED = "failed"
|
|
99
116
|
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
|
|
104
|
-
|
|
117
|
+
# The blocks Studio's schema gained after the base contract, each in its own
|
|
118
|
+
# lane: `idle` (seat parking L3) and `config` (provisioner L5). A strict 400
|
|
119
|
+
# naming one of them is an old Studio, not a bad snapshot.
|
|
120
|
+
OPTIONAL_BLOCKS = ("idle", "config")
|
|
121
|
+
|
|
122
|
+
# How long a snapshot leaves an optional block out after Studio refused it as
|
|
123
|
+
# an unknown key, before it offers the block again. An hour: one extra POST
|
|
124
|
+
# an hour against an old Studio, and a newly deployed one starts receiving
|
|
125
|
+
# the block within the hour.
|
|
126
|
+
BLOCK_REPROBE_S = 3600.0
|
|
127
|
+
IDLE_REPROBE_S = BLOCK_REPROBE_S
|
|
128
|
+
|
|
129
|
+
# The `config` block's keys (provisioner design §2.7 / lane L9), in order.
|
|
130
|
+
CONFIG_KEYS = ("feedOwnerActorId", "repos", "reviewersRequested", "ghLogin")
|
|
131
|
+
|
|
132
|
+
# The credentials a revloop container holds (the provisioner's variable
|
|
133
|
+
# collection, design §2.6, plus gh's own fallback name); the variable a
|
|
134
|
+
# `reviewer_token_env` names is added per config. No `config` value may carry
|
|
135
|
+
# one. A value shorter than SECRET_MIN is not a credential and is not matched
|
|
136
|
+
# (a passcode of "1" must not blank every login holding a 1).
|
|
137
|
+
SECRET_ENV = (
|
|
138
|
+
"GH_TOKEN", "GITHUB_TOKEN", "ALISSA_API_TOKEN", "ALISSA_UI_PASSCODE",
|
|
139
|
+
"ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN",
|
|
140
|
+
)
|
|
141
|
+
SECRET_MIN = 8
|
|
105
142
|
|
|
106
143
|
# The operator's lever per inbox kind, in the Factory's `lever` slot. The
|
|
107
144
|
# cap-out and the stability hold are both lifted by the same re-entry ack;
|
|
@@ -258,6 +295,7 @@ def build_snapshot(
|
|
|
258
295
|
queue_depth: "int | None",
|
|
259
296
|
repos: "tuple[str, ...]" = (),
|
|
260
297
|
now: "int | float | None" = None,
|
|
298
|
+
config: "Mapping | None" = None,
|
|
261
299
|
) -> dict:
|
|
262
300
|
"""The contract body for this pass, off the console's builders.
|
|
263
301
|
|
|
@@ -273,6 +311,9 @@ def build_snapshot(
|
|
|
273
311
|
timers from the newest poll snapshot, live / managed from the SAME roster
|
|
274
312
|
read as `sessions`, queued from the alissa queue, and `drainedAt` while
|
|
275
313
|
a drain holds.
|
|
314
|
+
|
|
315
|
+
`config` is the seat's `config_block`, or None to leave the key out (the
|
|
316
|
+
console's push, which has no derived answer to give).
|
|
276
317
|
"""
|
|
277
318
|
stamp = time.time() if now is None else now
|
|
278
319
|
snaps = sources.snapshots(DURATION_POINTS)
|
|
@@ -304,7 +345,7 @@ def build_snapshot(
|
|
|
304
345
|
live=live, managed=managed, queued=sources.queued(),
|
|
305
346
|
drain=sources.drain(), now=stamp,
|
|
306
347
|
)
|
|
307
|
-
|
|
348
|
+
snapshot = {
|
|
308
349
|
"schemaVersion": SCHEMA_VERSION,
|
|
309
350
|
"seat": SEAT,
|
|
310
351
|
"asOf": iso_utc(stamp),
|
|
@@ -326,6 +367,72 @@ def build_snapshot(
|
|
|
326
367
|
"inbox": inbox,
|
|
327
368
|
"idle": idle,
|
|
328
369
|
}
|
|
370
|
+
if config is not None:
|
|
371
|
+
snapshot["config"] = dict(config)
|
|
372
|
+
return snapshot
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _carries_secret(value: str, secrets: "Sequence[str]") -> bool:
|
|
376
|
+
return any(secret in value for secret in secrets)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def config_block(
|
|
380
|
+
config: Config,
|
|
381
|
+
*,
|
|
382
|
+
gh_login: "str | None",
|
|
383
|
+
environ: "Mapping[str, str] | None" = None,
|
|
384
|
+
) -> dict:
|
|
385
|
+
"""The seat's own word on what it derived (issue #159): four fields,
|
|
386
|
+
each from the config the daemon is RUNNING on, never from the raw env.
|
|
387
|
+
|
|
388
|
+
* `feedOwnerActorId` -- the bodies-of-work authority under
|
|
389
|
+
`repos_source: bows` (the token's own actor, resolved at boot, unless
|
|
390
|
+
`bow_owners` names one). Null in static mode (no feed is read) and when
|
|
391
|
+
several owners are trusted (there is no single answer to quote).
|
|
392
|
+
* `repos` -- the effective allowlist, sorted: under bows the derived
|
|
393
|
+
union the last refresh bound, in static mode the configured list
|
|
394
|
+
(empty there means "every repo that requests me").
|
|
395
|
+
* `reviewersRequested` -- always empty: the reviewer seat requests no
|
|
396
|
+
reviewer, it IS the one requested. The key is kept so every seat's
|
|
397
|
+
block has one shape.
|
|
398
|
+
* `ghLogin` -- the reviewer identity the gh credential resolves to (the
|
|
399
|
+
login round counting and every posted review are held to); null when
|
|
400
|
+
unknown.
|
|
401
|
+
|
|
402
|
+
`environ` (default: the process env) supplies the secrets to scan for:
|
|
403
|
+
a value carrying one is blanked -- a list entry dropped, a scalar
|
|
404
|
+
nulled -- and named in a WARNING by field only."""
|
|
405
|
+
env = os.environ if environ is None else environ
|
|
406
|
+
names = SECRET_ENV + (
|
|
407
|
+
(config.reviewer_token_env,) if config.reviewer_token_env else ()
|
|
408
|
+
)
|
|
409
|
+
secrets = [
|
|
410
|
+
v for k in names if len(v := (env.get(k) or "").strip()) >= SECRET_MIN
|
|
411
|
+
]
|
|
412
|
+
owners = config.bow_owners if config.repos_source == REPOS_BOWS else ()
|
|
413
|
+
block: dict = {
|
|
414
|
+
"feedOwnerActorId": owners[0] if len(owners) == 1 else None,
|
|
415
|
+
"repos": sorted(config.repos, key=str.casefold),
|
|
416
|
+
"reviewersRequested": [],
|
|
417
|
+
"ghLogin": gh_login or None,
|
|
418
|
+
}
|
|
419
|
+
for key in CONFIG_KEYS:
|
|
420
|
+
value = block[key]
|
|
421
|
+
if isinstance(value, list):
|
|
422
|
+
kept = [v for v in value if not _carries_secret(v, secrets)]
|
|
423
|
+
blanked = len(kept) != len(value)
|
|
424
|
+
block[key] = kept
|
|
425
|
+
else:
|
|
426
|
+
blanked = value is not None and _carries_secret(value, secrets)
|
|
427
|
+
if blanked:
|
|
428
|
+
block[key] = None
|
|
429
|
+
if blanked:
|
|
430
|
+
log.warning(
|
|
431
|
+
"fleet-vitals: the config block's %s carried a credential "
|
|
432
|
+
"this process holds — blanked before sending; check the "
|
|
433
|
+
"variable it is derived from", key,
|
|
434
|
+
)
|
|
435
|
+
return block
|
|
329
436
|
|
|
330
437
|
|
|
331
438
|
def body_bytes(snapshot: dict) -> int:
|
|
@@ -384,6 +491,7 @@ class FleetVitalsPusher:
|
|
|
384
491
|
client: AlissaClient,
|
|
385
492
|
*,
|
|
386
493
|
clock: Callable[[], float] = time.time,
|
|
494
|
+
config_reader: "Callable[[], Mapping | None] | None" = None,
|
|
387
495
|
):
|
|
388
496
|
self.config = config
|
|
389
497
|
self._sources = sources
|
|
@@ -394,9 +502,12 @@ class FleetVitalsPusher:
|
|
|
394
502
|
# every later refusal is a DEBUG line. The push itself is still
|
|
395
503
|
# attempted every pass -- the pass after activation lands on its own.
|
|
396
504
|
self._not_activated_warned = False
|
|
397
|
-
#
|
|
398
|
-
#
|
|
399
|
-
self.
|
|
505
|
+
# The `config` block's source (the daemon's `_vitals_config`), or
|
|
506
|
+
# None -- the console's pusher -- to leave the block out.
|
|
507
|
+
self._config_reader = config_reader
|
|
508
|
+
# Per optional block: until when it is left out (Studio refused it
|
|
509
|
+
# as an unknown key). Absent = offer it.
|
|
510
|
+
self._refused_until: "dict[str, float]" = {}
|
|
400
511
|
|
|
401
512
|
def build(self, *, heartbeat_at: "int | float", queue_depth: "int | None") -> dict:
|
|
402
513
|
"""This pass's contract body, fitted to the API's caps."""
|
|
@@ -407,8 +518,24 @@ class FleetVitalsPusher:
|
|
|
407
518
|
queue_depth=queue_depth,
|
|
408
519
|
repos=self.config.repos,
|
|
409
520
|
now=self._clock(),
|
|
521
|
+
config=self._config(),
|
|
410
522
|
))
|
|
411
523
|
|
|
524
|
+
def _config(self) -> "Mapping | None":
|
|
525
|
+
"""The `config` block, or None. A reader that fails costs the block,
|
|
526
|
+
never the snapshot: the rest of the vitals still go out."""
|
|
527
|
+
if self._config_reader is None:
|
|
528
|
+
return None
|
|
529
|
+
try:
|
|
530
|
+
return self._config_reader()
|
|
531
|
+
except Exception as exc:
|
|
532
|
+
log.warning(
|
|
533
|
+
"fleet-vitals: could not derive the config block (%s: %s) — "
|
|
534
|
+
"pushing the snapshot without it",
|
|
535
|
+
type(exc).__name__, exc,
|
|
536
|
+
)
|
|
537
|
+
return None
|
|
538
|
+
|
|
412
539
|
def push_once(
|
|
413
540
|
self, *, heartbeat_at: "int | float", queue_depth: "int | None" = None
|
|
414
541
|
) -> str:
|
|
@@ -431,8 +558,10 @@ class FleetVitalsPusher:
|
|
|
431
558
|
type(exc).__name__, exc,
|
|
432
559
|
)
|
|
433
560
|
return VITALS_FAILED
|
|
434
|
-
|
|
435
|
-
|
|
561
|
+
now = self._clock()
|
|
562
|
+
for key, until in self._refused_until.items():
|
|
563
|
+
if now < until:
|
|
564
|
+
snapshot.pop(key, None)
|
|
436
565
|
if self.config.dry_run:
|
|
437
566
|
log.info("[dry-run] would push fleet vitals (%s)", describe(snapshot))
|
|
438
567
|
return VITALS_SKIPPED
|
|
@@ -450,10 +579,18 @@ class FleetVitalsPusher:
|
|
|
450
579
|
return self.push_once(heartbeat_at=heartbeat, queue_depth=None)
|
|
451
580
|
|
|
452
581
|
@staticmethod
|
|
453
|
-
def
|
|
454
|
-
"""
|
|
455
|
-
predates seat parking
|
|
456
|
-
|
|
582
|
+
def _refused_blocks(exc: AlissaError, snapshot: dict) -> "list[str]":
|
|
583
|
+
"""The optional blocks of `snapshot` a 400 names: a Studio whose
|
|
584
|
+
strict schema predates them (seat parking's lane L3 adds `idle`, the
|
|
585
|
+
provisioner's lane L5 adds `config`). A word match, so `config` is
|
|
586
|
+
not read into an unrelated message that merely contains it."""
|
|
587
|
+
if exc.status != 400:
|
|
588
|
+
return []
|
|
589
|
+
detail = str(exc.detail)
|
|
590
|
+
return [
|
|
591
|
+
key for key in OPTIONAL_BLOCKS
|
|
592
|
+
if key in snapshot and re.search(rf"\b{key}\b", detail)
|
|
593
|
+
]
|
|
457
594
|
|
|
458
595
|
def _post(self, snapshot: dict) -> str:
|
|
459
596
|
try:
|
|
@@ -465,16 +602,21 @@ class FleetVitalsPusher:
|
|
|
465
602
|
self._warn_failed(exc)
|
|
466
603
|
return VITALS_FAILED
|
|
467
604
|
except AlissaError as exc:
|
|
468
|
-
|
|
469
|
-
|
|
605
|
+
refused = self._refused_blocks(exc, snapshot)
|
|
606
|
+
if refused:
|
|
607
|
+
until = self._clock() + BLOCK_REPROBE_S
|
|
608
|
+
for key in refused:
|
|
609
|
+
self._refused_until[key] = until
|
|
470
610
|
log.warning(
|
|
471
|
-
"fleet-vitals: Studio refused the
|
|
472
|
-
"
|
|
473
|
-
"
|
|
474
|
-
|
|
611
|
+
"fleet-vitals: Studio refused the %s block(s) (status "
|
|
612
|
+
"%s: %s) — its schema predates them; resending without "
|
|
613
|
+
"them, and leaving them out for %d min",
|
|
614
|
+
", ".join(f"`{k}`" for k in refused), exc.status,
|
|
615
|
+
exc.detail, int(BLOCK_REPROBE_S // 60),
|
|
475
616
|
)
|
|
476
|
-
trimmed =
|
|
477
|
-
|
|
617
|
+
trimmed = {
|
|
618
|
+
k: v for k, v in snapshot.items() if k not in refused
|
|
619
|
+
}
|
|
478
620
|
return self._post(trimmed)
|
|
479
621
|
self._warn_failed(exc)
|
|
480
622
|
return VITALS_FAILED
|
|
@@ -545,6 +687,7 @@ def build_pusher(
|
|
|
545
687
|
*,
|
|
546
688
|
github: object = None,
|
|
547
689
|
endpoint: "str | None" = None,
|
|
690
|
+
config_reader: "Callable[[], Mapping | None] | None" = None,
|
|
548
691
|
) -> FleetVitalsPusher:
|
|
549
692
|
"""The pusher the watcher wires in when `fleet_vitals_enabled` is on.
|
|
550
693
|
|
|
@@ -555,7 +698,9 @@ def build_pusher(
|
|
|
555
698
|
own `ALISSA_API_TOKEN`); its absence surfaces as the pusher's WARN, never
|
|
556
699
|
at construction, because a daemon must boot and poll whether or not
|
|
557
700
|
vitals can authenticate. The import is local because `sources` imports
|
|
558
|
-
`loop`, which imports this module.
|
|
701
|
+
`loop`, which imports this module. `config_reader` is the daemon's
|
|
702
|
+
`config` block source (issue #159); the console builds its own pusher
|
|
703
|
+
without one.
|
|
559
704
|
"""
|
|
560
705
|
from .version import version
|
|
561
706
|
from .webui.sources import Sources
|
|
@@ -565,4 +710,7 @@ def build_pusher(
|
|
|
565
710
|
running_version=version.value,
|
|
566
711
|
run=_console_runner(github),
|
|
567
712
|
)
|
|
568
|
-
return FleetVitalsPusher(
|
|
713
|
+
return FleetVitalsPusher(
|
|
714
|
+
config, sources, AlissaClient(base=endpoint),
|
|
715
|
+
config_reader=config_reader,
|
|
716
|
+
)
|
|
@@ -94,6 +94,7 @@ from .fleet_vitals import (
|
|
|
94
94
|
VITALS_SKIPPED,
|
|
95
95
|
FleetVitalsPusher,
|
|
96
96
|
build_pusher,
|
|
97
|
+
config_block as fleet_vitals_config_block,
|
|
97
98
|
)
|
|
98
99
|
from .loop_events import (
|
|
99
100
|
LoopEventsEmitter,
|
|
@@ -2494,7 +2495,8 @@ class ReviewWatcher:
|
|
|
2494
2495
|
# console sidecar need not be running.
|
|
2495
2496
|
self._fleet_vitals: FleetVitalsPusher | None = (
|
|
2496
2497
|
build_pusher(
|
|
2497
|
-
config, github=self.github, endpoint=config.alissa_endpoint
|
|
2498
|
+
config, github=self.github, endpoint=config.alissa_endpoint,
|
|
2499
|
+
config_reader=self._vitals_config,
|
|
2498
2500
|
)
|
|
2499
2501
|
if config.fleet_vitals_enabled
|
|
2500
2502
|
else None
|
|
@@ -7754,6 +7756,19 @@ class ReviewWatcher:
|
|
|
7754
7756
|
)
|
|
7755
7757
|
return VITALS_FAILED
|
|
7756
7758
|
|
|
7759
|
+
def _vitals_config(self) -> dict:
|
|
7760
|
+
"""The snapshot's `config` block (issue #159), read off what this
|
|
7761
|
+
watcher RUNS on: `self.config` is re-bound by every allowlist
|
|
7762
|
+
refresh (`_apply_repos`) and carries the feed authority
|
|
7763
|
+
`resolve_feed_authority` settled at boot, so the block reports the
|
|
7764
|
+
derived answer, not the configured one -- the pusher itself was built
|
|
7765
|
+
with the boot config. The login is the reviewer identity's (memoised
|
|
7766
|
+
after the first read; a failed read fails the block, never the
|
|
7767
|
+
push)."""
|
|
7768
|
+
return fleet_vitals_config_block(
|
|
7769
|
+
self.config, gh_login=self.github.login
|
|
7770
|
+
)
|
|
7771
|
+
|
|
7757
7772
|
def _note_ledger_unwritable(self) -> None:
|
|
7758
7773
|
"""Report a pass refused because the ledger cannot record it.
|
|
7759
7774
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.31.13
|
|
@@ -20,7 +20,7 @@ from ..config import Config, load_config_file, resolve_config_path
|
|
|
20
20
|
from ..fleet_vitals import FleetVitalsPusher
|
|
21
21
|
from ..version import version
|
|
22
22
|
from .auth import Auth, PasscodeUnset, require_passcode
|
|
23
|
-
from .server import App, make_server
|
|
23
|
+
from .server import App, bind_url, make_server
|
|
24
24
|
from .sources import Sources
|
|
25
25
|
|
|
26
26
|
# The default bind port. Deliberately NOT the devloop console's 8787: the two
|
|
@@ -52,7 +52,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
52
52
|
p.add_argument("--version", action="version",
|
|
53
53
|
version=f"%(prog)s {version.value}")
|
|
54
54
|
p.add_argument("--host", default="127.0.0.1", metavar="ADDR",
|
|
55
|
-
help="bind address (default: 127.0.0.1 -- localhost only)"
|
|
55
|
+
help="bind address (default: 127.0.0.1 -- localhost only); "
|
|
56
|
+
"an IPv6 literal binds IPv6, and `::` takes IPv4 too")
|
|
56
57
|
p.add_argument("--port", type=int, default=DEFAULT_PORT, metavar="PORT",
|
|
57
58
|
help=f"bind port (default: {DEFAULT_PORT})")
|
|
58
59
|
p.add_argument("--workspace-root", type=Path, default=None, metavar="PATH",
|
|
@@ -131,7 +132,7 @@ def main(argv: "list[str] | None" = None) -> int:
|
|
|
131
132
|
|
|
132
133
|
print(
|
|
133
134
|
f"alissa-revloop-ui {version.value} -- serving on "
|
|
134
|
-
f"
|
|
135
|
+
f"{bind_url(args.host, args.port)} (passcode required); "
|
|
135
136
|
f"watching {config.workspace_root}",
|
|
136
137
|
flush=True,
|
|
137
138
|
)
|
|
@@ -35,6 +35,7 @@ from __future__ import annotations
|
|
|
35
35
|
|
|
36
36
|
import json
|
|
37
37
|
import re
|
|
38
|
+
import socket
|
|
38
39
|
import sqlite3
|
|
39
40
|
import sys
|
|
40
41
|
import time
|
|
@@ -576,8 +577,34 @@ _REDIRECT_HOME = (
|
|
|
576
577
|
)
|
|
577
578
|
|
|
578
579
|
|
|
580
|
+
class _IPv6Server(ThreadingHTTPServer):
|
|
581
|
+
"""The console on an IPv6 address (issue #163). `::` is the container
|
|
582
|
+
default because Railway's private network (`*.railway.internal`) resolves
|
|
583
|
+
to IPv6 only, and an AF_INET socket refuses those peers outright.
|
|
584
|
+
IPV6_V6ONLY is cleared explicitly rather than inherited from
|
|
585
|
+
net.ipv6.bindv6only, so `::` also takes IPv4 (as v4-mapped peers) on any
|
|
586
|
+
host; on a specific address like `::1` the option is moot."""
|
|
587
|
+
|
|
588
|
+
address_family = socket.AF_INET6
|
|
589
|
+
|
|
590
|
+
def server_bind(self) -> None:
|
|
591
|
+
try:
|
|
592
|
+
self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
|
|
593
|
+
except (AttributeError, OSError):
|
|
594
|
+
pass # no dual-stack on this platform: `::` serves IPv6 only
|
|
595
|
+
super().server_bind()
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def bind_url(host: str, port: int) -> str:
|
|
599
|
+
"""The URL a bind serves, brackets around an IPv6 literal."""
|
|
600
|
+
return f"http://[{host}]:{port}" if ":" in host else f"http://{host}:{port}"
|
|
601
|
+
|
|
602
|
+
|
|
579
603
|
def make_server(app: App, host: str, port: int) -> ThreadingHTTPServer:
|
|
580
|
-
|
|
604
|
+
# An IPv6 literal (anything with a ':') needs an AF_INET6 socket; an IPv4
|
|
605
|
+
# address or a hostname keeps the stdlib's AF_INET default.
|
|
606
|
+
cls = _IPv6Server if ":" in host else ThreadingHTTPServer
|
|
607
|
+
server = cls((host, port), Handler)
|
|
581
608
|
server.daemon_threads = True
|
|
582
609
|
server.app = app # type: ignore[attr-defined]
|
|
583
610
|
return server
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.31.11
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{alissa_tools_github_revloop-0.31.11 → alissa_tools_github_revloop-0.31.13}/requirements.txt
RENAMED
|
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
|