alissa-tools-github-revloop 0.16.2__tar.gz → 0.16.4__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.16.2/src/main/alissa_tools_github_revloop.egg-info → alissa_tools_github_revloop-0.16.4}/PKG-INFO +1 -1
- {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.4}/src/main/alissa/tools/github/revloop/__main__.py +9 -0
- {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.4}/src/main/alissa/tools/github/revloop/config.py +117 -0
- alissa_tools_github_revloop-0.16.4/src/main/alissa/tools/github/revloop/ghclient.py +605 -0
- {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.4}/src/main/alissa/tools/github/revloop/loop.py +520 -8
- {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.4}/src/main/alissa/tools/github/revloop/proc.py +26 -5
- {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.4}/src/main/alissa/tools/github/revloop/state.py +192 -2
- alissa_tools_github_revloop-0.16.4/src/main/alissa/tools/github/revloop/version +1 -0
- {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.4}/src/main/alissa/tools/github/revloop/webui/page.py +5 -1
- {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.4/src/main/alissa_tools_github_revloop.egg-info}/PKG-INFO +1 -1
- alissa_tools_github_revloop-0.16.2/src/main/alissa/tools/github/revloop/ghclient.py +0 -292
- alissa_tools_github_revloop-0.16.2/src/main/alissa/tools/github/revloop/version +0 -1
- {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.4}/LICENSE +0 -0
- {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.4}/MANIFEST.in +0 -0
- {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.4}/NOTICE +0 -0
- {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.4}/README.md +0 -0
- {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.4}/requirements.txt +0 -0
- {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.4}/setup.cfg +0 -0
- {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.4}/setup.py +0 -0
- {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.4}/src/main/alissa/tools/github/revloop/__init__.py +0 -0
- {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.4}/src/main/alissa/tools/github/revloop/alissa.py +0 -0
- {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.4}/src/main/alissa/tools/github/revloop/prreview.py +0 -0
- {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.4}/src/main/alissa/tools/github/revloop/version.py +0 -0
- {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.4}/src/main/alissa/tools/github/revloop/webui/__init__.py +0 -0
- {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.4}/src/main/alissa/tools/github/revloop/webui/__main__.py +0 -0
- {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.4}/src/main/alissa/tools/github/revloop/webui/auth.py +0 -0
- {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.4}/src/main/alissa/tools/github/revloop/webui/server.py +0 -0
- {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.4}/src/main/alissa/tools/github/revloop/webui/sources.py +0 -0
- {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.4}/src/main/alissa/tools/github/revloop/webui/sysinfo.py +0 -0
- {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.4}/src/main/alissa_tools_github_revloop.egg-info/SOURCES.txt +0 -0
- {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.4}/src/main/alissa_tools_github_revloop.egg-info/dependency_links.txt +0 -0
- {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.4}/src/main/alissa_tools_github_revloop.egg-info/entry_points.txt +0 -0
- {alissa_tools_github_revloop-0.16.2 → alissa_tools_github_revloop-0.16.4}/src/main/alissa_tools_github_revloop.egg-info/top_level.txt +0 -0
|
@@ -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,89 @@ 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
|
+
# The per-character half of the same rule, used to name the offending
|
|
27
|
+
# characters in an error without echoing the value (see _describe).
|
|
28
|
+
_ENV_CHAR_RE = re.compile(r"[A-Za-z0-9_]")
|
|
29
|
+
|
|
30
|
+
# ...which, on its own, accepts every GitHub credential format: `ghp_`, `gho_`,
|
|
31
|
+
# `ghu_`, `ghs_`, `ghr_` and `github_pat_` tokens are `[A-Za-z0-9_]` throughout
|
|
32
|
+
# and therefore valid identifiers. So the shape has to be rejected explicitly,
|
|
33
|
+
# or the one mistake this validation exists to catch -- pasting the token where
|
|
34
|
+
# its variable's NAME belongs -- sails through.
|
|
35
|
+
_TOKEN_SHAPE_RE = re.compile(r"^(gh[pousr]_|github_pat_)", re.IGNORECASE)
|
|
36
|
+
|
|
37
|
+
# Generous on purpose. A tighter ceiling (32 was tried) rejects names an
|
|
38
|
+
# operator can plausibly write -- `REVLOOP_REVIEWER_GITHUB_TOKEN_ENV` is 33 --
|
|
39
|
+
# and this bound is NOT what catches credentials: MIN_SECRET_RUN_LENGTH is,
|
|
40
|
+
# and it catches a 40-character hex token on its shape whatever the ceiling
|
|
41
|
+
# says. So the ceiling exists only to refuse the obviously absurd, and it is
|
|
42
|
+
# deliberately kept OUT of `_is_credential_shaped` (see there).
|
|
43
|
+
MAX_ENV_NAME_LENGTH = 40
|
|
44
|
+
|
|
45
|
+
# Length is not the whole discriminator, because a secret can be short enough
|
|
46
|
+
# to fit under the ceiling. The shape is: a long, undifferentiated run of
|
|
47
|
+
# alphanumerics. Names are conventionally SCREAMING_SNAKE and separated -- the
|
|
48
|
+
# underscore is what a legacy 40-hex token and a generic alnum API key lack.
|
|
49
|
+
MIN_SECRET_RUN_LENGTH = 20
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _is_credential_shaped(value: str) -> bool:
|
|
53
|
+
"""Whether a `reviewer_token_env` value looks like a pasted secret.
|
|
54
|
+
|
|
55
|
+
A heuristic, and deliberately one that errs toward accusing: the cost of a
|
|
56
|
+
false positive is renaming a variable, and the cost of a false negative is
|
|
57
|
+
a live credential sitting in a config file on a shared volume.
|
|
58
|
+
|
|
59
|
+
Length is deliberately NOT one of the clauses. It was, and it made a merely
|
|
60
|
+
LONG name get classified as a credential -- rejected, redacted, and told to
|
|
61
|
+
rotate itself. Over-length is a separate refusal with its own message; only
|
|
62
|
+
an actual credential SHAPE drives redaction.
|
|
63
|
+
"""
|
|
64
|
+
return bool(
|
|
65
|
+
_TOKEN_SHAPE_RE.match(value)
|
|
66
|
+
or (
|
|
67
|
+
len(value) >= MIN_SECRET_RUN_LENGTH
|
|
68
|
+
and "_" not in value
|
|
69
|
+
and value.isalnum()
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _describe(value: str) -> str:
|
|
75
|
+
"""Say what is wrong with a rejected value without reprinting a secret.
|
|
76
|
+
|
|
77
|
+
`__main__` prints this to stderr as `config error: …`, straight into the
|
|
78
|
+
container log, so a pasted credential must never be echoed there. But the
|
|
79
|
+
other way to fail this check is an ordinary typo
|
|
80
|
+
(`REVLOOP-REVIEWER-GH-TOKEN`), and there the value IS the diagnostic.
|
|
81
|
+
|
|
82
|
+
Redacting everything loses the typo case; echoing whatever the GitHub-shape
|
|
83
|
+
heuristic does not recognise hands back the container-log disclosure for
|
|
84
|
+
every non-GitHub secret (`sk-proj-…`, `xoxb-…`, `glpat-…` all carry
|
|
85
|
+
punctuation and match no clause above). So neither branch prints the value:
|
|
86
|
+
what an operator needs is WHICH CHARACTERS were rejected, and those can be
|
|
87
|
+
named with their positions and nothing else.
|
|
88
|
+
"""
|
|
89
|
+
if _is_credential_shaped(value):
|
|
90
|
+
return f"a {len(value)}-character value that looks like a credential"
|
|
91
|
+
offenders = sorted({c for c in value if not _ENV_CHAR_RE.match(c)})
|
|
92
|
+
if offenders:
|
|
93
|
+
where = ", ".join(
|
|
94
|
+
f"{c!r} at {', '.join(str(i) for i, ch in enumerate(value) if ch == c)}"
|
|
95
|
+
for c in offenders
|
|
96
|
+
)
|
|
97
|
+
return f"a {len(value)}-character value containing {where}"
|
|
98
|
+
return f"a {len(value)}-character value"
|
|
99
|
+
|
|
100
|
+
|
|
22
101
|
# What to do when a PR has a pending review request but no matching Alissa
|
|
23
102
|
# review task (CR2 is implementer-side, so a third-party PR may not have one).
|
|
24
103
|
ON_MISSING_SPAWN = "spawn_anyway" # review anyway, PR URL carries the context
|
|
@@ -44,6 +123,7 @@ CONFIG_KEYS = (
|
|
|
44
123
|
"operators",
|
|
45
124
|
"agent_profile",
|
|
46
125
|
"reviewer_login",
|
|
126
|
+
"reviewer_token_env",
|
|
47
127
|
"state_path",
|
|
48
128
|
"on_missing_review_task",
|
|
49
129
|
"on_missing_hub",
|
|
@@ -117,6 +197,15 @@ class Config:
|
|
|
117
197
|
agent_profile: str = "claude"
|
|
118
198
|
reviewer_login: str | None = None # None -> resolve once via `gh api user`
|
|
119
199
|
|
|
200
|
+
# NAME of the environment variable holding the reviewer identity's GitHub
|
|
201
|
+
# token -- never the token itself, which has no business in a config file
|
|
202
|
+
# on a mounted volume. Set it and every `gh` call the daemon makes runs
|
|
203
|
+
# under that credential explicitly, with the container's inherited
|
|
204
|
+
# GH_TOKEN/GITHUB_TOKEN stripped; leave it None and the daemon inherits,
|
|
205
|
+
# which is the pre-#51 behaviour and warns loudly at preflight because a
|
|
206
|
+
# shared container is exactly where inheritance picks the wrong identity.
|
|
207
|
+
reviewer_token_env: str | None = None
|
|
208
|
+
|
|
120
209
|
# None means "derive from the workspace" -- read `state_db` for the
|
|
121
210
|
# resolved location, never this field.
|
|
122
211
|
state_path: Path | None = None
|
|
@@ -247,6 +336,33 @@ class Config:
|
|
|
247
336
|
# state of a working loop -- an alarm that always fires is noise.
|
|
248
337
|
raise ValueError(f"reap_session_cap must be >= 1, got {session_cap}")
|
|
249
338
|
|
|
339
|
+
token_env = raw.get("reviewer_token_env")
|
|
340
|
+
if token_env is not None:
|
|
341
|
+
token_env = str(token_env).strip()
|
|
342
|
+
# The overwhelmingly likely way to get here is pasting the TOKEN in
|
|
343
|
+
# place of the variable's name, which would leave a live credential
|
|
344
|
+
# in a config file on a shared volume and then fail as "unset
|
|
345
|
+
# variable" — an error that reads as though the secret were missing
|
|
346
|
+
# rather than exposed. So the check is two-sided: it must LOOK like
|
|
347
|
+
# a name, and it must not look like a credential. The value itself
|
|
348
|
+
# is never echoed back (see _redact).
|
|
349
|
+
if (
|
|
350
|
+
not _ENV_NAME_RE.match(token_env)
|
|
351
|
+
or _is_credential_shaped(token_env)
|
|
352
|
+
or len(token_env) > MAX_ENV_NAME_LENGTH
|
|
353
|
+
):
|
|
354
|
+
rotate = (
|
|
355
|
+
" If that is a credential, rotate it: it is now in a config file."
|
|
356
|
+
if _is_credential_shaped(token_env)
|
|
357
|
+
else ""
|
|
358
|
+
)
|
|
359
|
+
raise ValueError(
|
|
360
|
+
f"reviewer_token_env must be an environment variable NAME "
|
|
361
|
+
f"(e.g. 'REVLOOP_REVIEWER_GH_TOKEN') of at most "
|
|
362
|
+
f"{MAX_ENV_NAME_LENGTH} characters, not a value or a "
|
|
363
|
+
f"token — got {_describe(token_env)}.{rotate}"
|
|
364
|
+
)
|
|
365
|
+
|
|
250
366
|
state_path = raw.get("state_path")
|
|
251
367
|
return cls(
|
|
252
368
|
workspace_root=Path(workspace_root),
|
|
@@ -257,6 +373,7 @@ class Config:
|
|
|
257
373
|
operators=operators,
|
|
258
374
|
agent_profile=raw.get("agent_profile", "claude"),
|
|
259
375
|
reviewer_login=raw.get("reviewer_login"),
|
|
376
|
+
reviewer_token_env=token_env or None,
|
|
260
377
|
state_path=Path(state_path).expanduser() if state_path else None,
|
|
261
378
|
on_missing_review_task=mode,
|
|
262
379
|
on_missing_hub=hub_mode,
|