alissa-tools-github-revloop 0.16.2__tar.gz → 0.16.3__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 (33) hide show
  1. {alissa_tools_github_revloop-0.16.2/src/main/alissa_tools_github_revloop.egg-info → alissa_tools_github_revloop-0.16.3}/PKG-INFO +1 -1
  2. {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.3}/src/main/alissa/tools/github/revloop/__main__.py +9 -0
  3. {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.3}/src/main/alissa/tools/github/revloop/config.py +88 -0
  4. alissa_tools_github_revloop-0.16.3/src/main/alissa/tools/github/revloop/ghclient.py +596 -0
  5. {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.3}/src/main/alissa/tools/github/revloop/loop.py +508 -8
  6. {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.3}/src/main/alissa/tools/github/revloop/proc.py +26 -5
  7. {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.3}/src/main/alissa/tools/github/revloop/state.py +171 -2
  8. alissa_tools_github_revloop-0.16.3/src/main/alissa/tools/github/revloop/version +1 -0
  9. {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.3}/src/main/alissa/tools/github/revloop/webui/page.py +3 -1
  10. {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.3/src/main/alissa_tools_github_revloop.egg-info}/PKG-INFO +1 -1
  11. alissa_tools_github_revloop-0.16.2/src/main/alissa/tools/github/revloop/ghclient.py +0 -292
  12. alissa_tools_github_revloop-0.16.2/src/main/alissa/tools/github/revloop/version +0 -1
  13. {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.3}/LICENSE +0 -0
  14. {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.3}/MANIFEST.in +0 -0
  15. {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.3}/NOTICE +0 -0
  16. {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.3}/README.md +0 -0
  17. {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.3}/requirements.txt +0 -0
  18. {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.3}/setup.cfg +0 -0
  19. {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.3}/setup.py +0 -0
  20. {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.3}/src/main/alissa/tools/github/revloop/__init__.py +0 -0
  21. {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.3}/src/main/alissa/tools/github/revloop/alissa.py +0 -0
  22. {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.3}/src/main/alissa/tools/github/revloop/prreview.py +0 -0
  23. {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.3}/src/main/alissa/tools/github/revloop/version.py +0 -0
  24. {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.3}/src/main/alissa/tools/github/revloop/webui/__init__.py +0 -0
  25. {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.3}/src/main/alissa/tools/github/revloop/webui/__main__.py +0 -0
  26. {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.3}/src/main/alissa/tools/github/revloop/webui/auth.py +0 -0
  27. {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.3}/src/main/alissa/tools/github/revloop/webui/server.py +0 -0
  28. {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.3}/src/main/alissa/tools/github/revloop/webui/sources.py +0 -0
  29. {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.3}/src/main/alissa/tools/github/revloop/webui/sysinfo.py +0 -0
  30. {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.3}/src/main/alissa_tools_github_revloop.egg-info/SOURCES.txt +0 -0
  31. {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.3}/src/main/alissa_tools_github_revloop.egg-info/dependency_links.txt +0 -0
  32. {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.3}/src/main/alissa_tools_github_revloop.egg-info/entry_points.txt +0 -0
  33. {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.3}/src/main/alissa_tools_github_revloop.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alissa-tools-github-revloop
3
- Version: 0.16.2
3
+ Version: 0.16.3
4
4
  Summary: ALISSA-TOOLS-GITHUB-REVLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera
@@ -94,6 +94,14 @@ def build_parser() -> argparse.ArgumentParser:
94
94
  over.add_argument("--hub-template", metavar="TEMPLATE")
95
95
  over.add_argument("--agent-profile", metavar="NAME")
96
96
  over.add_argument("--reviewer-login", metavar="LOGIN")
97
+ over.add_argument(
98
+ "--reviewer-token-env",
99
+ metavar="VAR",
100
+ help="NAME of the environment variable holding the reviewer "
101
+ "identity's GitHub token (never the token itself). Set it and every "
102
+ "`gh` call runs under that credential explicitly instead of whatever "
103
+ "the container happened to export",
104
+ )
97
105
  over.add_argument("--state-path", type=Path, metavar="PATH")
98
106
  over.add_argument(
99
107
  "--on-missing-review-task",
@@ -143,6 +151,7 @@ def overrides_from(args: argparse.Namespace) -> dict:
143
151
  "hub_template": args.hub_template,
144
152
  "agent_profile": args.agent_profile,
145
153
  "reviewer_login": args.reviewer_login,
154
+ "reviewer_token_env": args.reviewer_token_env,
146
155
  "state_path": args.state_path,
147
156
  "on_missing_review_task": args.on_missing_review_task,
148
157
  "on_missing_hub": args.on_missing_hub,
@@ -15,10 +15,65 @@ daemons over different workspaces on the same machine, each pointed with
15
15
  from __future__ import annotations
16
16
 
17
17
  import json
18
+ import re
18
19
  from dataclasses import dataclass
19
20
  from pathlib import Path
20
21
  from typing import Any, Mapping
21
22
 
23
+ # A POSIX-ish environment variable name -- what `reviewer_token_env` must be.
24
+ _ENV_NAME_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
25
+
26
+ # ...which, on its own, accepts every GitHub credential format: `ghp_`, `gho_`,
27
+ # `ghu_`, `ghs_`, `ghr_` and `github_pat_` tokens are `[A-Za-z0-9_]` throughout
28
+ # and therefore valid identifiers. So the shape has to be rejected explicitly,
29
+ # or the one mistake this validation exists to catch -- pasting the token where
30
+ # its variable's NAME belongs -- sails through.
31
+ _TOKEN_SHAPE_RE = re.compile(r"^(gh[pousr]_|github_pat_)", re.IGNORECASE)
32
+
33
+ # No environment variable name comes close (`REVLOOP_REVIEWER_GH_TOKEN` is 25);
34
+ # every GitHub token, including the 40-character legacy hex ones, exceeds it.
35
+ MAX_ENV_NAME_LENGTH = 32
36
+
37
+ # Length is not the whole discriminator, because a secret can be short enough
38
+ # to fit under the ceiling. The shape is: a long, undifferentiated run of
39
+ # alphanumerics. Names are conventionally SCREAMING_SNAKE and separated -- the
40
+ # underscore is what a legacy 40-hex token and a generic alnum API key lack.
41
+ MIN_SECRET_RUN_LENGTH = 20
42
+
43
+
44
+ def _is_credential_shaped(value: str) -> bool:
45
+ """Whether a `reviewer_token_env` value looks like a pasted secret.
46
+
47
+ A heuristic, and deliberately one that errs toward accusing: the cost of a
48
+ false positive is renaming a variable, and the cost of a false negative is
49
+ a live credential sitting in a config file on a shared volume.
50
+ """
51
+ return bool(
52
+ _TOKEN_SHAPE_RE.match(value)
53
+ or len(value) > MAX_ENV_NAME_LENGTH
54
+ or (
55
+ len(value) >= MIN_SECRET_RUN_LENGTH
56
+ and "_" not in value
57
+ and value.isalnum()
58
+ )
59
+ )
60
+
61
+
62
+ def _describe(value: str) -> str:
63
+ """Name a rejected value in the error -- redacted only when it has to be.
64
+
65
+ `__main__` prints this to stderr as `config error: …`, straight into the
66
+ container log, so a pasted secret must not be duplicated there: those get
67
+ length plus a short prefix. But the other way to fail this check is an
68
+ ordinary typo (`REVLOOP-REVIEWER-GH-TOKEN`), and there the value IS the
69
+ diagnostic -- redacting it turns a one-glance fix into a puzzle. The two
70
+ are separable now that credential shapes are identified explicitly.
71
+ """
72
+ if _is_credential_shaped(value):
73
+ return f'a {len(value)}-character value starting "{value[:4]}…"'
74
+ return repr(value)
75
+
76
+
22
77
  # What to do when a PR has a pending review request but no matching Alissa
23
78
  # review task (CR2 is implementer-side, so a third-party PR may not have one).
24
79
  ON_MISSING_SPAWN = "spawn_anyway" # review anyway, PR URL carries the context
@@ -44,6 +99,7 @@ CONFIG_KEYS = (
44
99
  "operators",
45
100
  "agent_profile",
46
101
  "reviewer_login",
102
+ "reviewer_token_env",
47
103
  "state_path",
48
104
  "on_missing_review_task",
49
105
  "on_missing_hub",
@@ -117,6 +173,15 @@ class Config:
117
173
  agent_profile: str = "claude"
118
174
  reviewer_login: str | None = None # None -> resolve once via `gh api user`
119
175
 
176
+ # NAME of the environment variable holding the reviewer identity's GitHub
177
+ # token -- never the token itself, which has no business in a config file
178
+ # on a mounted volume. Set it and every `gh` call the daemon makes runs
179
+ # under that credential explicitly, with the container's inherited
180
+ # GH_TOKEN/GITHUB_TOKEN stripped; leave it None and the daemon inherits,
181
+ # which is the pre-#51 behaviour and warns loudly at preflight because a
182
+ # shared container is exactly where inheritance picks the wrong identity.
183
+ reviewer_token_env: str | None = None
184
+
120
185
  # None means "derive from the workspace" -- read `state_db` for the
121
186
  # resolved location, never this field.
122
187
  state_path: Path | None = None
@@ -247,6 +312,28 @@ class Config:
247
312
  # state of a working loop -- an alarm that always fires is noise.
248
313
  raise ValueError(f"reap_session_cap must be >= 1, got {session_cap}")
249
314
 
315
+ token_env = raw.get("reviewer_token_env")
316
+ if token_env is not None:
317
+ token_env = str(token_env).strip()
318
+ # The overwhelmingly likely way to get here is pasting the TOKEN in
319
+ # place of the variable's name, which would leave a live credential
320
+ # in a config file on a shared volume and then fail as "unset
321
+ # variable" — an error that reads as though the secret were missing
322
+ # rather than exposed. So the check is two-sided: it must LOOK like
323
+ # a name, and it must not look like a credential. The value itself
324
+ # is never echoed back (see _redact).
325
+ if not _ENV_NAME_RE.match(token_env) or _is_credential_shaped(token_env):
326
+ rotate = (
327
+ " If that is a credential, rotate it: it is now in a config file."
328
+ if _is_credential_shaped(token_env)
329
+ else ""
330
+ )
331
+ raise ValueError(
332
+ f"reviewer_token_env must be an environment variable NAME "
333
+ f"(e.g. 'REVLOOP_REVIEWER_GH_TOKEN'), not a value or a "
334
+ f"token — got {_describe(token_env)}.{rotate}"
335
+ )
336
+
250
337
  state_path = raw.get("state_path")
251
338
  return cls(
252
339
  workspace_root=Path(workspace_root),
@@ -257,6 +344,7 @@ class Config:
257
344
  operators=operators,
258
345
  agent_profile=raw.get("agent_profile", "claude"),
259
346
  reviewer_login=raw.get("reviewer_login"),
347
+ reviewer_token_env=token_env or None,
260
348
  state_path=Path(state_path).expanduser() if state_path else None,
261
349
  on_missing_review_task=mode,
262
350
  on_missing_hub=hub_mode,