alissa-tools-github-devloop 0.8.37__tar.gz → 0.8.38__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 (39) hide show
  1. {alissa_tools_github_devloop-0.8.37/src/main/alissa_tools_github_devloop.egg-info → alissa_tools_github_devloop-0.8.38}/PKG-INFO +1 -1
  2. {alissa_tools_github_devloop-0.8.37 → alissa_tools_github_devloop-0.8.38}/src/main/alissa/tools/github/devloop/fleet_vitals.py +153 -25
  3. {alissa_tools_github_devloop-0.8.37 → alissa_tools_github_devloop-0.8.38}/src/main/alissa/tools/github/devloop/loop.py +13 -1
  4. alissa_tools_github_devloop-0.8.38/src/main/alissa/tools/github/devloop/version +1 -0
  5. {alissa_tools_github_devloop-0.8.37 → alissa_tools_github_devloop-0.8.38/src/main/alissa_tools_github_devloop.egg-info}/PKG-INFO +1 -1
  6. alissa_tools_github_devloop-0.8.37/src/main/alissa/tools/github/devloop/version +0 -1
  7. {alissa_tools_github_devloop-0.8.37 → alissa_tools_github_devloop-0.8.38}/LICENSE +0 -0
  8. {alissa_tools_github_devloop-0.8.37 → alissa_tools_github_devloop-0.8.38}/MANIFEST.in +0 -0
  9. {alissa_tools_github_devloop-0.8.37 → alissa_tools_github_devloop-0.8.38}/NOTICE +0 -0
  10. {alissa_tools_github_devloop-0.8.37 → alissa_tools_github_devloop-0.8.38}/README.md +0 -0
  11. {alissa_tools_github_devloop-0.8.37 → alissa_tools_github_devloop-0.8.38}/requirements.txt +0 -0
  12. {alissa_tools_github_devloop-0.8.37 → alissa_tools_github_devloop-0.8.38}/setup.cfg +0 -0
  13. {alissa_tools_github_devloop-0.8.37 → alissa_tools_github_devloop-0.8.38}/setup.py +0 -0
  14. {alissa_tools_github_devloop-0.8.37 → alissa_tools_github_devloop-0.8.38}/src/main/alissa/tools/github/devloop/__init__.py +0 -0
  15. {alissa_tools_github_devloop-0.8.37 → alissa_tools_github_devloop-0.8.38}/src/main/alissa/tools/github/devloop/__main__.py +0 -0
  16. {alissa_tools_github_devloop-0.8.37 → alissa_tools_github_devloop-0.8.38}/src/main/alissa/tools/github/devloop/alissa.py +0 -0
  17. {alissa_tools_github_devloop-0.8.37 → alissa_tools_github_devloop-0.8.38}/src/main/alissa/tools/github/devloop/alissa_client.py +0 -0
  18. {alissa_tools_github_devloop-0.8.37 → alissa_tools_github_devloop-0.8.38}/src/main/alissa/tools/github/devloop/bows.py +0 -0
  19. {alissa_tools_github_devloop-0.8.37 → alissa_tools_github_devloop-0.8.38}/src/main/alissa/tools/github/devloop/config.py +0 -0
  20. {alissa_tools_github_devloop-0.8.37 → alissa_tools_github_devloop-0.8.38}/src/main/alissa/tools/github/devloop/ghclient.py +0 -0
  21. {alissa_tools_github_devloop-0.8.37 → alissa_tools_github_devloop-0.8.38}/src/main/alissa/tools/github/devloop/hubs.py +0 -0
  22. {alissa_tools_github_devloop-0.8.37 → alissa_tools_github_devloop-0.8.38}/src/main/alissa/tools/github/devloop/janitor.py +0 -0
  23. {alissa_tools_github_devloop-0.8.37 → alissa_tools_github_devloop-0.8.38}/src/main/alissa/tools/github/devloop/loop_events.py +0 -0
  24. {alissa_tools_github_devloop-0.8.37 → alissa_tools_github_devloop-0.8.38}/src/main/alissa/tools/github/devloop/parking.py +0 -0
  25. {alissa_tools_github_devloop-0.8.37 → alissa_tools_github_devloop-0.8.38}/src/main/alissa/tools/github/devloop/proc.py +0 -0
  26. {alissa_tools_github_devloop-0.8.37 → alissa_tools_github_devloop-0.8.38}/src/main/alissa/tools/github/devloop/prompts.py +0 -0
  27. {alissa_tools_github_devloop-0.8.37 → alissa_tools_github_devloop-0.8.38}/src/main/alissa/tools/github/devloop/state.py +0 -0
  28. {alissa_tools_github_devloop-0.8.37 → alissa_tools_github_devloop-0.8.38}/src/main/alissa/tools/github/devloop/version.py +0 -0
  29. {alissa_tools_github_devloop-0.8.37 → alissa_tools_github_devloop-0.8.38}/src/main/alissa/tools/github/devloop/webui/__init__.py +0 -0
  30. {alissa_tools_github_devloop-0.8.37 → alissa_tools_github_devloop-0.8.38}/src/main/alissa/tools/github/devloop/webui/__main__.py +0 -0
  31. {alissa_tools_github_devloop-0.8.37 → alissa_tools_github_devloop-0.8.38}/src/main/alissa/tools/github/devloop/webui/auth.py +0 -0
  32. {alissa_tools_github_devloop-0.8.37 → alissa_tools_github_devloop-0.8.38}/src/main/alissa/tools/github/devloop/webui/page.py +0 -0
  33. {alissa_tools_github_devloop-0.8.37 → alissa_tools_github_devloop-0.8.38}/src/main/alissa/tools/github/devloop/webui/server.py +0 -0
  34. {alissa_tools_github_devloop-0.8.37 → alissa_tools_github_devloop-0.8.38}/src/main/alissa/tools/github/devloop/webui/sources.py +0 -0
  35. {alissa_tools_github_devloop-0.8.37 → alissa_tools_github_devloop-0.8.38}/src/main/alissa/tools/github/devloop/webui/sysinfo.py +0 -0
  36. {alissa_tools_github_devloop-0.8.37 → alissa_tools_github_devloop-0.8.38}/src/main/alissa_tools_github_devloop.egg-info/SOURCES.txt +0 -0
  37. {alissa_tools_github_devloop-0.8.37 → alissa_tools_github_devloop-0.8.38}/src/main/alissa_tools_github_devloop.egg-info/dependency_links.txt +0 -0
  38. {alissa_tools_github_devloop-0.8.37 → alissa_tools_github_devloop-0.8.38}/src/main/alissa_tools_github_devloop.egg-info/entry_points.txt +0 -0
  39. {alissa_tools_github_devloop-0.8.37 → alissa_tools_github_devloop-0.8.38}/src/main/alissa_tools_github_devloop.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alissa-tools-github-devloop
3
- Version: 0.8.37
3
+ Version: 0.8.38
4
4
  Summary: ALISSA-TOOLS-GITHUB-DEVLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera
@@ -45,19 +45,34 @@ answers (`FleetVitalsPusher.push_now`). Until Studio's schema carries the
45
45
  block (the design's lane L3), a strict 400 naming `idle` makes the pusher
46
46
  resend without it and leave it out for IDLE_REPROBE_S: the vitals the Factory
47
47
  already renders never go dark over a field nobody reads yet.
48
+
49
+ The managed fleet's smoke (issue #155, studio.alissa.app provisioner design
50
+ §2.7, lane L9) adds the `config` block -- what this seat DERIVED, in its own
51
+ words: the feed owner actor id, the effective repository allowlist, the
52
+ reviewer login(s) it requests and its own GitHub login. SM2-SM4 compare it to
53
+ what Studio provisioned. Four fields, every one of them an identity or a
54
+ name, never a credential: the block is built from typed config fields, not
55
+ from the environment, and any value that carries a secret the process holds
56
+ (`SECRET_ENV`) is blanked before it can leave. The daemon's own pass sends
57
+ it; the console's out-of-pass push leaves it out (that process never
58
+ resolves the feed authority nor refreshes the allowlist, so its answer would
59
+ be a different one). A Studio whose strict schema predates the block gets
60
+ the `idle` treatment: resent without it, offered again after BLOCK_REPROBE_S.
48
61
  """
49
62
 
50
63
  from __future__ import annotations
51
64
 
52
65
  import json
53
66
  import logging
67
+ import os
68
+ import re
54
69
  import time
55
70
  from datetime import datetime, timezone
56
71
  from typing import Callable, Mapping, Sequence
57
72
 
58
73
  from . import parking
59
74
  from .alissa_client import AlissaAuthError, AlissaClient, AlissaError
60
- from .config import Config
75
+ from .config import REPOS_BOWS, Config
61
76
  from .version import version as running_version
62
77
  from .webui import sources as sources_mod
63
78
  from .webui import sysinfo
@@ -84,11 +99,30 @@ PUSHED = "pushed"
84
99
  SKIPPED = "skipped"
85
100
  FAILED = "failed"
86
101
 
87
- # How long a snapshot leaves the `idle` block out after Studio refused it as
88
- # an unknown key (a schema that predates seat parking), before it offers the
89
- # block again. An hour: one extra POST an hour against an old Studio, and a
90
- # newly deployed one starts receiving the block within the hour.
91
- IDLE_REPROBE_S = 3600.0
102
+ # The blocks Studio's schema gained after the base contract, each in its own
103
+ # lane: `idle` (seat parking L3) and `config` (provisioner L5). A strict 400
104
+ # naming one of them is an old Studio, not a bad snapshot.
105
+ OPTIONAL_BLOCKS = ("idle", "config")
106
+
107
+ # How long a snapshot leaves an optional block out after Studio refused it as
108
+ # an unknown key, before it offers the block again. An hour: one extra POST
109
+ # an hour against an old Studio, and a newly deployed one starts receiving
110
+ # the block within the hour.
111
+ BLOCK_REPROBE_S = 3600.0
112
+ IDLE_REPROBE_S = BLOCK_REPROBE_S
113
+
114
+ # The `config` block's keys (provisioner design §2.7 / lane L9), in order.
115
+ CONFIG_KEYS = ("feedOwnerActorId", "repos", "reviewersRequested", "ghLogin")
116
+
117
+ # The credentials a devloop container holds (the provisioner's variable
118
+ # collection, design §2.6, plus gh's own fallback name). No `config` value
119
+ # may carry one; a shorter value than SECRET_MIN is not a credential and is
120
+ # not matched (a passcode of "1" must not blank every login holding a 1).
121
+ SECRET_ENV = (
122
+ "GH_TOKEN", "GITHUB_TOKEN", "ALISSA_API_TOKEN", "ALISSA_UI_PASSCODE",
123
+ "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN",
124
+ )
125
+ SECRET_MIN = 8
92
126
 
93
127
 
94
128
  def _session_grammar() -> "Callable[[str], object]":
@@ -207,6 +241,7 @@ def build_snapshot(
207
241
  parse_session: "Callable[[str], object] | None" = None,
208
242
  queued: "int | None" = None,
209
243
  drain: "Mapping | None" = None,
244
+ config: "Mapping | None" = None,
210
245
  ) -> "dict":
211
246
  """The contract body for one pass, from the console's builders.
212
247
 
@@ -222,7 +257,10 @@ def build_snapshot(
222
257
  `queued` (the undispatched alissa queue items, None when unread) and
223
258
  `drain` (the holding drain row, or None) feed the `idle` block, whose
224
259
  owed / timers come from the latest poll snapshot and whose live /
225
- managed come from `roster` -- the same reading as `sessions`."""
260
+ managed come from `roster` -- the same reading as `sessions`.
261
+
262
+ `config` is the seat's `config_block`, or None to leave the key out
263
+ (the console's push, which has no derived answer to give)."""
226
264
  parse = parse_session or _session_grammar()
227
265
  now_ts = heartbeat_at if now is None else now
228
266
 
@@ -276,7 +314,7 @@ def build_snapshot(
276
314
  )
277
315
 
278
316
  drift = sources.drift()
279
- return {
317
+ snapshot = {
280
318
  "schemaVersion": SCHEMA_VERSION,
281
319
  "seat": SEAT,
282
320
  "asOf": iso(now_ts),
@@ -294,6 +332,65 @@ def build_snapshot(
294
332
  "inbox": inbox,
295
333
  "idle": idle,
296
334
  }
335
+ if config is not None:
336
+ snapshot["config"] = dict(config)
337
+ return snapshot
338
+
339
+
340
+ def _carries_secret(value: str, secrets: "Sequence[str]") -> bool:
341
+ return any(secret in value for secret in secrets)
342
+
343
+
344
+ def config_block(
345
+ config: Config,
346
+ *,
347
+ gh_login: "str | None",
348
+ environ: "Mapping[str, str] | None" = None,
349
+ ) -> "dict":
350
+ """The seat's own word on what it derived (issue #155): four fields,
351
+ each from the config the daemon is RUNNING on, never from the raw env.
352
+
353
+ * `feedOwnerActorId` -- the bodies-of-work authority under
354
+ `repos_source: bows` (the token's own actor unless `bow_owners` names
355
+ one). Null in static mode (no feed is read) and when several owners
356
+ are trusted (there is no single answer to quote).
357
+ * `repos` -- the effective allowlist, sorted: under bows the derived
358
+ union the last refresh bound, in static mode the configured list.
359
+ * `reviewersRequested` -- the login(s) the handoff requests
360
+ (`ALISSA_DEV_REVIEWERS` and its fallbacks).
361
+ * `ghLogin` -- the account the gh token belongs to; null when unknown.
362
+
363
+ `environ` (default: the process env) supplies the secrets to scan for:
364
+ a value carrying one is blanked -- a list entry dropped, a scalar
365
+ nulled -- and named in a WARNING by field only."""
366
+ env = os.environ if environ is None else environ
367
+ secrets = [
368
+ v for k in SECRET_ENV if len(v := (env.get(k) or "").strip()) >= SECRET_MIN
369
+ ]
370
+ owners = config.bow_owners if config.repos_source == REPOS_BOWS else ()
371
+ block: "dict" = {
372
+ "feedOwnerActorId": owners[0] if len(owners) == 1 else None,
373
+ "repos": sorted(config.repos, key=str.casefold),
374
+ "reviewersRequested": list(config.reviewers),
375
+ "ghLogin": gh_login or None,
376
+ }
377
+ for key in CONFIG_KEYS:
378
+ value = block[key]
379
+ if isinstance(value, list):
380
+ kept = [v for v in value if not _carries_secret(v, secrets)]
381
+ blanked = len(kept) != len(value)
382
+ block[key] = kept
383
+ else:
384
+ blanked = value is not None and _carries_secret(value, secrets)
385
+ if blanked:
386
+ block[key] = None
387
+ if blanked:
388
+ log.warning(
389
+ "fleet vitals: the config block's %s carried a credential "
390
+ "this process holds -- blanked before sending; check the "
391
+ "variable it is derived from", key,
392
+ )
393
+ return block
297
394
 
298
395
 
299
396
  def encoded_size(snapshot: "Mapping") -> int:
@@ -342,6 +439,7 @@ class FleetVitalsPusher:
342
439
  sources: "Sources | None" = None,
343
440
  memory_reader: "Callable[[], Mapping | None]" = sysinfo.memory_usage,
344
441
  clock: "Callable[[], float]" = time.time,
442
+ config_reader: "Callable[[], Mapping | None] | None" = None,
345
443
  ):
346
444
  self.config = config
347
445
  self.client = client
@@ -350,10 +448,13 @@ class FleetVitalsPusher:
350
448
  )
351
449
  self._memory = memory_reader
352
450
  self._clock = clock
451
+ # The `config` block's source (the daemon's `_vitals_config`), or
452
+ # None -- the console's pusher -- to leave the block out.
453
+ self._config_reader = config_reader
353
454
  self._not_activated_warned = False
354
- # Until when the `idle` block is left out (Studio refused it as an
355
- # unknown key); 0 = offer it.
356
- self._idle_refused_until = 0.0
455
+ # Per optional block: until when it is left out (Studio refused it
456
+ # as an unknown key). Absent = offer it.
457
+ self._refused_until: "dict[str, float]" = {}
357
458
  self.last: "dict | None" = None
358
459
 
359
460
  def push_pass(self, roster: "Sequence[Mapping] | None") -> str:
@@ -373,6 +474,20 @@ class FleetVitalsPusher:
373
474
  heartbeat = snaps[0]["ts"] if snaps else now
374
475
  return self._push(roster, heartbeat, now)
375
476
 
477
+ def _config(self) -> "Mapping | None":
478
+ """The `config` block, or None. A reader that fails costs the block,
479
+ never the snapshot: the rest of the vitals still go out."""
480
+ if self._config_reader is None:
481
+ return None
482
+ try:
483
+ return self._config_reader()
484
+ except Exception as exc:
485
+ log.warning(
486
+ "fleet vitals: could not derive the config block (%s) — "
487
+ "pushing the snapshot without it", exc,
488
+ )
489
+ return None
490
+
376
491
  def _push(
377
492
  self, roster: "Sequence[Mapping] | None", heartbeat: float, now: float,
378
493
  ) -> str:
@@ -381,6 +496,7 @@ class FleetVitalsPusher:
381
496
  self.sources, roster=roster, heartbeat_at=heartbeat,
382
497
  now=now, memory=self._memory(),
383
498
  queued=self.sources.queued(), drain=self.sources.drain(),
499
+ config=self._config(),
384
500
  ))
385
501
  except Exception as exc:
386
502
  log.warning(
@@ -388,16 +504,25 @@ class FleetVitalsPusher:
388
504
  "pushed this pass; the next pass builds a fresh one", exc,
389
505
  )
390
506
  return FAILED
391
- if now < self._idle_refused_until:
392
- snapshot.pop("idle", None)
507
+ for key, until in self._refused_until.items():
508
+ if now < until:
509
+ snapshot.pop(key, None)
393
510
  self.last = snapshot
394
511
  return self._post(snapshot)
395
512
 
396
513
  @staticmethod
397
- def _refuses_idle(exc: AlissaError) -> bool:
398
- """A 400 that names the `idle` key: a Studio whose strict schema
399
- predates seat parking (the design's lane L3 adds the field)."""
400
- return exc.status == 400 and "idle" in str(exc.detail)
514
+ def _refused_blocks(exc: AlissaError, snapshot: "Mapping") -> "list[str]":
515
+ """The optional blocks of `snapshot` a 400 names: a Studio whose
516
+ strict schema predates them (seat parking's lane L3 adds `idle`, the
517
+ provisioner's lane L5 adds `config`). A word match, so `config`
518
+ is not read into an unrelated message that merely contains it."""
519
+ if exc.status != 400:
520
+ return []
521
+ detail = str(exc.detail)
522
+ return [
523
+ key for key in OPTIONAL_BLOCKS
524
+ if key in snapshot and re.search(rf"\b{key}\b", detail)
525
+ ]
401
526
 
402
527
  def _post(self, snapshot: "dict") -> str:
403
528
  try:
@@ -409,16 +534,19 @@ class FleetVitalsPusher:
409
534
  self._warn_failed(exc)
410
535
  return FAILED
411
536
  except AlissaError as exc:
412
- if "idle" in snapshot and self._refuses_idle(exc):
413
- self._idle_refused_until = self._clock() + IDLE_REPROBE_S
537
+ refused = self._refused_blocks(exc, snapshot)
538
+ if refused:
539
+ until = self._clock() + BLOCK_REPROBE_S
540
+ for key in refused:
541
+ self._refused_until[key] = until
414
542
  log.warning(
415
- "fleet vitals: Studio refused the seat-parking `idle` "
416
- "block (status %s: %s) — its schema predates it; "
417
- "resending without it, and leaving it out for %d min",
418
- exc.status, exc.detail, int(IDLE_REPROBE_S // 60),
543
+ "fleet vitals: Studio refused the %s block(s) (status "
544
+ "%s: %s) — its schema predates them; resending without "
545
+ "them, and leaving them out for %d min",
546
+ ", ".join(f"`{k}`" for k in refused), exc.status,
547
+ exc.detail, int(BLOCK_REPROBE_S // 60),
419
548
  )
420
- trimmed = dict(snapshot)
421
- trimmed.pop("idle")
549
+ trimmed = {k: v for k, v in snapshot.items() if k not in refused}
422
550
  self.last = trimmed
423
551
  return self._post(trimmed)
424
552
  self._warn_failed(exc)
@@ -3432,7 +3432,8 @@ class DevWatcher:
3432
3432
  # injectable so a test never opens a socket.
3433
3433
  self.fleet_vitals = fleet_vitals or (
3434
3434
  fleet_vitals_mod.FleetVitalsPusher(
3435
- config, AlissaClient(base=config.alissa_endpoint)
3435
+ config, AlissaClient(base=config.alissa_endpoint),
3436
+ config_reader=self._vitals_config,
3436
3437
  )
3437
3438
  if config.fleet_vitals_enabled
3438
3439
  else None
@@ -11296,6 +11297,17 @@ class DevWatcher:
11296
11297
  return fleet_vitals_mod.SKIPPED
11297
11298
  return self.fleet_vitals.push_pass(roster.entries)
11298
11299
 
11300
+ def _vitals_config(self) -> dict:
11301
+ """The snapshot's `config` block (issue #155), read off what this
11302
+ watcher RUNS on: `self.config` is re-bound by every allowlist
11303
+ refresh and carries the feed authority `resolve_feed_authority`
11304
+ settled at boot, so the block reports the derived answer, not the
11305
+ configured one. The login is the gh token's (memoised after the
11306
+ first read; a failed read fails the block, never the push)."""
11307
+ return fleet_vitals_mod.config_block(
11308
+ self.config, gh_login=self.github.login
11309
+ )
11310
+
11299
11311
  def _emit_loop_events(
11300
11312
  self,
11301
11313
  issue_results: "list[tuple[str, Decision]]",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alissa-tools-github-devloop
3
- Version: 0.8.37
3
+ Version: 0.8.38
4
4
  Summary: ALISSA-TOOLS-GITHUB-DEVLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera