alissa-tools-github-revloop 0.31.10__tar.gz → 0.31.12__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.10/src/main/alissa_tools_github_revloop.egg-info → alissa_tools_github_revloop-0.31.12}/PKG-INFO +1 -1
- {alissa_tools_github_revloop-0.31.10 → alissa_tools_github_revloop-0.31.12}/src/main/alissa/tools/github/revloop/fleet_vitals.py +217 -5
- {alissa_tools_github_revloop-0.31.10 → alissa_tools_github_revloop-0.31.12}/src/main/alissa/tools/github/revloop/loop.py +211 -7
- alissa_tools_github_revloop-0.31.12/src/main/alissa/tools/github/revloop/parking.py +222 -0
- {alissa_tools_github_revloop-0.31.10 → alissa_tools_github_revloop-0.31.12}/src/main/alissa/tools/github/revloop/state.py +124 -0
- alissa_tools_github_revloop-0.31.12/src/main/alissa/tools/github/revloop/version +1 -0
- {alissa_tools_github_revloop-0.31.10 → alissa_tools_github_revloop-0.31.12}/src/main/alissa/tools/github/revloop/webui/__main__.py +20 -1
- {alissa_tools_github_revloop-0.31.10 → alissa_tools_github_revloop-0.31.12}/src/main/alissa/tools/github/revloop/webui/server.py +114 -2
- {alissa_tools_github_revloop-0.31.10 → alissa_tools_github_revloop-0.31.12}/src/main/alissa/tools/github/revloop/webui/sources.py +77 -3
- {alissa_tools_github_revloop-0.31.10 → alissa_tools_github_revloop-0.31.12/src/main/alissa_tools_github_revloop.egg-info}/PKG-INFO +1 -1
- {alissa_tools_github_revloop-0.31.10 → alissa_tools_github_revloop-0.31.12}/src/main/alissa_tools_github_revloop.egg-info/SOURCES.txt +1 -0
- alissa_tools_github_revloop-0.31.10/src/main/alissa/tools/github/revloop/version +0 -1
- {alissa_tools_github_revloop-0.31.10 → alissa_tools_github_revloop-0.31.12}/LICENSE +0 -0
- {alissa_tools_github_revloop-0.31.10 → alissa_tools_github_revloop-0.31.12}/MANIFEST.in +0 -0
- {alissa_tools_github_revloop-0.31.10 → alissa_tools_github_revloop-0.31.12}/NOTICE +0 -0
- {alissa_tools_github_revloop-0.31.10 → alissa_tools_github_revloop-0.31.12}/README.md +0 -0
- {alissa_tools_github_revloop-0.31.10 → alissa_tools_github_revloop-0.31.12}/requirements.txt +0 -0
- {alissa_tools_github_revloop-0.31.10 → alissa_tools_github_revloop-0.31.12}/setup.cfg +0 -0
- {alissa_tools_github_revloop-0.31.10 → alissa_tools_github_revloop-0.31.12}/setup.py +0 -0
- {alissa_tools_github_revloop-0.31.10 → alissa_tools_github_revloop-0.31.12}/src/main/alissa/tools/github/revloop/__init__.py +0 -0
- {alissa_tools_github_revloop-0.31.10 → alissa_tools_github_revloop-0.31.12}/src/main/alissa/tools/github/revloop/__main__.py +0 -0
- {alissa_tools_github_revloop-0.31.10 → alissa_tools_github_revloop-0.31.12}/src/main/alissa/tools/github/revloop/alissa.py +0 -0
- {alissa_tools_github_revloop-0.31.10 → alissa_tools_github_revloop-0.31.12}/src/main/alissa/tools/github/revloop/alissa_client.py +0 -0
- {alissa_tools_github_revloop-0.31.10 → alissa_tools_github_revloop-0.31.12}/src/main/alissa/tools/github/revloop/bows.py +0 -0
- {alissa_tools_github_revloop-0.31.10 → alissa_tools_github_revloop-0.31.12}/src/main/alissa/tools/github/revloop/config.py +0 -0
- {alissa_tools_github_revloop-0.31.10 → alissa_tools_github_revloop-0.31.12}/src/main/alissa/tools/github/revloop/ghclient.py +0 -0
- {alissa_tools_github_revloop-0.31.10 → alissa_tools_github_revloop-0.31.12}/src/main/alissa/tools/github/revloop/loop_events.py +0 -0
- {alissa_tools_github_revloop-0.31.10 → alissa_tools_github_revloop-0.31.12}/src/main/alissa/tools/github/revloop/proc.py +0 -0
- {alissa_tools_github_revloop-0.31.10 → alissa_tools_github_revloop-0.31.12}/src/main/alissa/tools/github/revloop/prompts.py +0 -0
- {alissa_tools_github_revloop-0.31.10 → alissa_tools_github_revloop-0.31.12}/src/main/alissa/tools/github/revloop/prreview.py +0 -0
- {alissa_tools_github_revloop-0.31.10 → alissa_tools_github_revloop-0.31.12}/src/main/alissa/tools/github/revloop/trust.py +0 -0
- {alissa_tools_github_revloop-0.31.10 → alissa_tools_github_revloop-0.31.12}/src/main/alissa/tools/github/revloop/version.py +0 -0
- {alissa_tools_github_revloop-0.31.10 → alissa_tools_github_revloop-0.31.12}/src/main/alissa/tools/github/revloop/webui/__init__.py +0 -0
- {alissa_tools_github_revloop-0.31.10 → alissa_tools_github_revloop-0.31.12}/src/main/alissa/tools/github/revloop/webui/auth.py +0 -0
- {alissa_tools_github_revloop-0.31.10 → alissa_tools_github_revloop-0.31.12}/src/main/alissa/tools/github/revloop/webui/page.py +0 -0
- {alissa_tools_github_revloop-0.31.10 → alissa_tools_github_revloop-0.31.12}/src/main/alissa/tools/github/revloop/webui/sysinfo.py +0 -0
- {alissa_tools_github_revloop-0.31.10 → alissa_tools_github_revloop-0.31.12}/src/main/alissa_tools_github_revloop.egg-info/dependency_links.txt +0 -0
- {alissa_tools_github_revloop-0.31.10 → alissa_tools_github_revloop-0.31.12}/src/main/alissa_tools_github_revloop.egg-info/entry_points.txt +0 -0
- {alissa_tools_github_revloop-0.31.10 → alissa_tools_github_revloop-0.31.12}/src/main/alissa_tools_github_revloop.egg-info/top_level.txt +0 -0
|
@@ -41,16 +41,43 @@ Design rules, all load-bearing:
|
|
|
41
41
|
user has not activated the loop app) warns ONCE per boot with the activate
|
|
42
42
|
URL and logs at DEBUG after, because it is a fixed condition the operator
|
|
43
43
|
resolves out of band, and the next pass after activation lands on its own.
|
|
44
|
+
|
|
45
|
+
Seat parking (issue #157, studio.alissa.app seat-parking design §2.2/§2.4)
|
|
46
|
+
adds the `idle` block -- the seat's own idle verdict (`parking.idle_block`),
|
|
47
|
+
with `drainedAt` while a drain holds -- so Studio can re-check a park against
|
|
48
|
+
the seat's OWN snapshot. The console's drain pushes one snapshot before it
|
|
49
|
+
answers (`FleetVitalsPusher.push_now`). Until Studio's schema carries the
|
|
50
|
+
block (the design's lane L3), a strict 400 naming `idle` makes the pusher
|
|
51
|
+
resend without it and leave it out for IDLE_REPROBE_S: the vitals the Factory
|
|
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.
|
|
44
68
|
"""
|
|
45
69
|
|
|
46
70
|
from __future__ import annotations
|
|
47
71
|
|
|
48
72
|
import json
|
|
49
73
|
import logging
|
|
74
|
+
import os
|
|
75
|
+
import re
|
|
50
76
|
import time
|
|
51
77
|
from datetime import datetime, timezone
|
|
52
|
-
from typing import TYPE_CHECKING, Any, Callable
|
|
78
|
+
from typing import TYPE_CHECKING, Any, Callable, Mapping, Sequence
|
|
53
79
|
|
|
80
|
+
from . import parking
|
|
54
81
|
from .alissa import parse_session_name, session_repo_slug
|
|
55
82
|
from .alissa_client import (
|
|
56
83
|
NOT_ACTIVATED,
|
|
@@ -58,7 +85,7 @@ from .alissa_client import (
|
|
|
58
85
|
AlissaClient,
|
|
59
86
|
AlissaError,
|
|
60
87
|
)
|
|
61
|
-
from .config import Config
|
|
88
|
+
from .config import REPOS_BOWS, Config
|
|
62
89
|
from .proc import CommandError, run as proc_run
|
|
63
90
|
|
|
64
91
|
if TYPE_CHECKING: # pragma: no cover - typing only
|
|
@@ -87,6 +114,32 @@ VITALS_PUSHED = "pushed"
|
|
|
87
114
|
VITALS_SKIPPED = "skipped"
|
|
88
115
|
VITALS_FAILED = "failed"
|
|
89
116
|
|
|
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
|
|
142
|
+
|
|
90
143
|
# The operator's lever per inbox kind, in the Factory's `lever` slot. The
|
|
91
144
|
# cap-out and the stability hold are both lifted by the same re-entry ack;
|
|
92
145
|
# a stalled episode is the console's retry-now (or a reap).
|
|
@@ -242,6 +295,7 @@ def build_snapshot(
|
|
|
242
295
|
queue_depth: "int | None",
|
|
243
296
|
repos: "tuple[str, ...]" = (),
|
|
244
297
|
now: "int | float | None" = None,
|
|
298
|
+
config: "Mapping | None" = None,
|
|
245
299
|
) -> dict:
|
|
246
300
|
"""The contract body for this pass, off the console's builders.
|
|
247
301
|
|
|
@@ -252,6 +306,14 @@ def build_snapshot(
|
|
|
252
306
|
optional field degrades to null on its own -- a broken tmux does not
|
|
253
307
|
blank the memory split, and vice versa -- and `kpis` is always null (an
|
|
254
308
|
orcloop-only section).
|
|
309
|
+
|
|
310
|
+
`idle` (seat parking, issue #157) is the seat's idle block: owed and
|
|
311
|
+
timers from the newest poll snapshot, live / managed from the SAME roster
|
|
312
|
+
read as `sessions`, queued from the alissa queue, and `drainedAt` while
|
|
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).
|
|
255
317
|
"""
|
|
256
318
|
stamp = time.time() if now is None else now
|
|
257
319
|
snaps = sources.snapshots(DURATION_POINTS)
|
|
@@ -277,7 +339,13 @@ def build_snapshot(
|
|
|
277
339
|
)
|
|
278
340
|
session_list = [_session_row(r, repos) for r in ordered[:LIST_CAP]]
|
|
279
341
|
inbox = [_inbox_row(item) for item in sources.inbox()["live"][:LIST_CAP]]
|
|
280
|
-
|
|
342
|
+
live, managed = parking.roster_counts(rows)
|
|
343
|
+
idle = parking.idle_block(
|
|
344
|
+
snaps[0] if snaps else None,
|
|
345
|
+
live=live, managed=managed, queued=sources.queued(),
|
|
346
|
+
drain=sources.drain(), now=stamp,
|
|
347
|
+
)
|
|
348
|
+
snapshot = {
|
|
281
349
|
"schemaVersion": SCHEMA_VERSION,
|
|
282
350
|
"seat": SEAT,
|
|
283
351
|
"asOf": iso_utc(stamp),
|
|
@@ -297,7 +365,74 @@ def build_snapshot(
|
|
|
297
365
|
"queueDepth": _int_or_none(queue_depth),
|
|
298
366
|
"kpis": None,
|
|
299
367
|
"inbox": inbox,
|
|
368
|
+
"idle": idle,
|
|
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,
|
|
300
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
|
|
301
436
|
|
|
302
437
|
|
|
303
438
|
def body_bytes(snapshot: dict) -> int:
|
|
@@ -356,6 +491,7 @@ class FleetVitalsPusher:
|
|
|
356
491
|
client: AlissaClient,
|
|
357
492
|
*,
|
|
358
493
|
clock: Callable[[], float] = time.time,
|
|
494
|
+
config_reader: "Callable[[], Mapping | None] | None" = None,
|
|
359
495
|
):
|
|
360
496
|
self.config = config
|
|
361
497
|
self._sources = sources
|
|
@@ -366,6 +502,12 @@ class FleetVitalsPusher:
|
|
|
366
502
|
# every later refusal is a DEBUG line. The push itself is still
|
|
367
503
|
# attempted every pass -- the pass after activation lands on its own.
|
|
368
504
|
self._not_activated_warned = False
|
|
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]" = {}
|
|
369
511
|
|
|
370
512
|
def build(self, *, heartbeat_at: "int | float", queue_depth: "int | None") -> dict:
|
|
371
513
|
"""This pass's contract body, fitted to the API's caps."""
|
|
@@ -376,8 +518,24 @@ class FleetVitalsPusher:
|
|
|
376
518
|
queue_depth=queue_depth,
|
|
377
519
|
repos=self.config.repos,
|
|
378
520
|
now=self._clock(),
|
|
521
|
+
config=self._config(),
|
|
379
522
|
))
|
|
380
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
|
+
|
|
381
539
|
def push_once(
|
|
382
540
|
self, *, heartbeat_at: "int | float", queue_depth: "int | None" = None
|
|
383
541
|
) -> str:
|
|
@@ -400,9 +558,41 @@ class FleetVitalsPusher:
|
|
|
400
558
|
type(exc).__name__, exc,
|
|
401
559
|
)
|
|
402
560
|
return VITALS_FAILED
|
|
561
|
+
now = self._clock()
|
|
562
|
+
for key, until in self._refused_until.items():
|
|
563
|
+
if now < until:
|
|
564
|
+
snapshot.pop(key, None)
|
|
403
565
|
if self.config.dry_run:
|
|
404
566
|
log.info("[dry-run] would push fleet vitals (%s)", describe(snapshot))
|
|
405
567
|
return VITALS_SKIPPED
|
|
568
|
+
return self._post(snapshot)
|
|
569
|
+
|
|
570
|
+
def push_now(self) -> str:
|
|
571
|
+
"""The console's out-of-pass push (the drain handshake, issue #157):
|
|
572
|
+
the same snapshot, its `heartbeatAt` the daemon's LAST completed pass
|
|
573
|
+
-- the console completes none of its own, and stamping now would read
|
|
574
|
+
as a heartbeat the loop never gave -- and `asOf` now. `queueDepth` is
|
|
575
|
+
null: the slot queue lives in the daemon's memory, and the console
|
|
576
|
+
cannot say. One of VITALS_PUSHED, VITALS_SKIPPED or VITALS_FAILED."""
|
|
577
|
+
snaps = self._sources.snapshots(1)
|
|
578
|
+
heartbeat = snaps[0]["ts"] if snaps else self._clock()
|
|
579
|
+
return self.push_once(heartbeat_at=heartbeat, queue_depth=None)
|
|
580
|
+
|
|
581
|
+
@staticmethod
|
|
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
|
+
]
|
|
594
|
+
|
|
595
|
+
def _post(self, snapshot: dict) -> str:
|
|
406
596
|
try:
|
|
407
597
|
result = self._client.post_fleet_vitals(snapshot)
|
|
408
598
|
except AlissaAuthError as exc:
|
|
@@ -412,6 +602,22 @@ class FleetVitalsPusher:
|
|
|
412
602
|
self._warn_failed(exc)
|
|
413
603
|
return VITALS_FAILED
|
|
414
604
|
except AlissaError as exc:
|
|
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
|
|
610
|
+
log.warning(
|
|
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),
|
|
616
|
+
)
|
|
617
|
+
trimmed = {
|
|
618
|
+
k: v for k, v in snapshot.items() if k not in refused
|
|
619
|
+
}
|
|
620
|
+
return self._post(trimmed)
|
|
415
621
|
self._warn_failed(exc)
|
|
416
622
|
return VITALS_FAILED
|
|
417
623
|
log.info(
|
|
@@ -481,6 +687,7 @@ def build_pusher(
|
|
|
481
687
|
*,
|
|
482
688
|
github: object = None,
|
|
483
689
|
endpoint: "str | None" = None,
|
|
690
|
+
config_reader: "Callable[[], Mapping | None] | None" = None,
|
|
484
691
|
) -> FleetVitalsPusher:
|
|
485
692
|
"""The pusher the watcher wires in when `fleet_vitals_enabled` is on.
|
|
486
693
|
|
|
@@ -491,7 +698,9 @@ def build_pusher(
|
|
|
491
698
|
own `ALISSA_API_TOKEN`); its absence surfaces as the pusher's WARN, never
|
|
492
699
|
at construction, because a daemon must boot and poll whether or not
|
|
493
700
|
vitals can authenticate. The import is local because `sources` imports
|
|
494
|
-
`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.
|
|
495
704
|
"""
|
|
496
705
|
from .version import version
|
|
497
706
|
from .webui.sources import Sources
|
|
@@ -501,4 +710,7 @@ def build_pusher(
|
|
|
501
710
|
running_version=version.value,
|
|
502
711
|
run=_console_runner(github),
|
|
503
712
|
)
|
|
504
|
-
return FleetVitalsPusher(
|
|
713
|
+
return FleetVitalsPusher(
|
|
714
|
+
config, sources, AlissaClient(base=endpoint),
|
|
715
|
+
config_reader=config_reader,
|
|
716
|
+
)
|