hermes-github-app-plugin 0.1.2__py3-none-any.whl → 0.3.0__py3-none-any.whl
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.
- hermes_github_app_plugin/auth.py +67 -17
- hermes_github_app_plugin/cli.py +113 -27
- hermes_github_app_plugin/config.py +89 -6
- hermes_github_app_plugin/plugin.yaml +9 -3
- hermes_github_app_plugin/skills/github-app-workflow/SKILL.md +19 -1
- hermes_github_app_plugin/tools.py +6 -2
- {hermes_github_app_plugin-0.1.2.dist-info → hermes_github_app_plugin-0.3.0.dist-info}/METADATA +43 -5
- hermes_github_app_plugin-0.3.0.dist-info/RECORD +14 -0
- {hermes_github_app_plugin-0.1.2.dist-info → hermes_github_app_plugin-0.3.0.dist-info}/WHEEL +1 -1
- hermes_github_app_plugin-0.1.2.dist-info/RECORD +0 -14
- {hermes_github_app_plugin-0.1.2.dist-info → hermes_github_app_plugin-0.3.0.dist-info}/entry_points.txt +0 -0
- {hermes_github_app_plugin-0.1.2.dist-info → hermes_github_app_plugin-0.3.0.dist-info}/licenses/LICENSE +0 -0
hermes_github_app_plugin/auth.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import re
|
|
5
6
|
import time
|
|
6
7
|
from dataclasses import dataclass
|
|
7
8
|
from datetime import datetime, timedelta, timezone
|
|
@@ -11,9 +12,10 @@ from urllib.parse import urlparse
|
|
|
11
12
|
import httpx
|
|
12
13
|
import jwt
|
|
13
14
|
|
|
14
|
-
from .config import GitHubAppConfig
|
|
15
|
+
from .config import NONO_PHANTOM_TOKEN, GitHubAppConfig
|
|
15
16
|
|
|
16
17
|
_MIN_REDACT_LENGTH = 8
|
|
18
|
+
_BROKERED_TOKEN_LIFETIME = timedelta(hours=1)
|
|
17
19
|
|
|
18
20
|
|
|
19
21
|
@dataclass(frozen=True)
|
|
@@ -25,6 +27,7 @@ class InstallationToken:
|
|
|
25
27
|
installation_id: str
|
|
26
28
|
client_id: str
|
|
27
29
|
app_slug: str | None
|
|
30
|
+
token_broker: str | None = None
|
|
28
31
|
|
|
29
32
|
@property
|
|
30
33
|
def redacted(self) -> str:
|
|
@@ -40,31 +43,60 @@ class GitHubAppAuth:
|
|
|
40
43
|
def __init__(self, config: GitHubAppConfig, client: httpx.Client | None = None) -> None:
|
|
41
44
|
self._config = config
|
|
42
45
|
self._client = client or httpx.Client(timeout=20)
|
|
43
|
-
self.
|
|
46
|
+
self._cached_tokens: dict[str, InstallationToken] = {}
|
|
44
47
|
|
|
45
48
|
@property
|
|
46
49
|
def config(self) -> GitHubAppConfig:
|
|
47
50
|
return self._config
|
|
48
51
|
|
|
49
52
|
def create_jwt(self) -> str:
|
|
50
|
-
"""Create a GitHub App JWT for installation-token exchange.
|
|
53
|
+
"""Create a GitHub App JWT for installation-token exchange.
|
|
54
|
+
|
|
55
|
+
In token broker mode there is no key to sign with; the placeholder is sent and the
|
|
56
|
+
broker replaces it outside the sandbox.
|
|
57
|
+
"""
|
|
58
|
+
if self._config.token_broker:
|
|
59
|
+
return NONO_PHANTOM_TOKEN
|
|
51
60
|
now = int(time.time())
|
|
52
61
|
payload = {"iat": now - 60, "exp": now + 9 * 60, "iss": self._config.client_id}
|
|
53
62
|
encoded = jwt.encode(payload, self._config.private_key, algorithm="RS256")
|
|
54
63
|
return str(encoded)
|
|
55
64
|
|
|
56
|
-
def get_installation_token(
|
|
65
|
+
def get_installation_token(
|
|
66
|
+
self, *, repo: str | None = None, force_refresh: bool = False
|
|
67
|
+
) -> InstallationToken:
|
|
57
68
|
"""Return a valid installation token, refreshing when near expiry."""
|
|
69
|
+
installation_id = self._config.installation_id_for_repo(repo)
|
|
70
|
+
cached_token = self._cached_tokens.get(installation_id)
|
|
58
71
|
if (
|
|
59
72
|
not force_refresh
|
|
60
|
-
and
|
|
61
|
-
and
|
|
73
|
+
and cached_token is not None
|
|
74
|
+
and cached_token.expires_at > datetime.now(timezone.utc) + timedelta(minutes=5)
|
|
62
75
|
):
|
|
63
|
-
return
|
|
76
|
+
return cached_token
|
|
64
77
|
|
|
78
|
+
if self._config.token_broker:
|
|
79
|
+
token = self._brokered_token(installation_id)
|
|
80
|
+
else:
|
|
81
|
+
token = self._exchange_app_jwt(installation_id)
|
|
82
|
+
self._cached_tokens[installation_id] = token
|
|
83
|
+
return token
|
|
84
|
+
|
|
85
|
+
def _brokered_token(self, installation_id: str) -> InstallationToken:
|
|
86
|
+
"""Return the placeholder token with no network call; the broker injects the real one."""
|
|
87
|
+
return InstallationToken(
|
|
88
|
+
token=NONO_PHANTOM_TOKEN,
|
|
89
|
+
expires_at=datetime.now(timezone.utc) + _BROKERED_TOKEN_LIFETIME,
|
|
90
|
+
installation_id=installation_id,
|
|
91
|
+
client_id=self._config.client_id,
|
|
92
|
+
app_slug=self._config.app_slug,
|
|
93
|
+
token_broker=self._config.token_broker,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def _exchange_app_jwt(self, installation_id: str) -> InstallationToken:
|
|
97
|
+
"""Exchange the app JWT for an installation access token."""
|
|
65
98
|
response = self._client.post(
|
|
66
|
-
f"{self._config.github_api_url}/app/installations/"
|
|
67
|
-
f"{self._config.installation_id}/access_tokens",
|
|
99
|
+
f"{self._config.github_api_url}/app/installations/{installation_id}/access_tokens",
|
|
68
100
|
headers={
|
|
69
101
|
"Accept": "application/vnd.github+json",
|
|
70
102
|
"Authorization": f"Bearer {self.create_jwt()}",
|
|
@@ -75,15 +107,13 @@ class GitHubAppAuth:
|
|
|
75
107
|
data = response.json()
|
|
76
108
|
expires_at_raw = str(data["expires_at"])
|
|
77
109
|
expires_at = datetime.fromisoformat(expires_at_raw.replace("Z", "+00:00"))
|
|
78
|
-
|
|
110
|
+
return InstallationToken(
|
|
79
111
|
token=str(data["token"]),
|
|
80
112
|
expires_at=expires_at,
|
|
81
|
-
installation_id=
|
|
113
|
+
installation_id=installation_id,
|
|
82
114
|
client_id=self._config.client_id,
|
|
83
115
|
app_slug=self._config.app_slug,
|
|
84
116
|
)
|
|
85
|
-
self._cached_token = token
|
|
86
|
-
return token
|
|
87
117
|
|
|
88
118
|
def app_request(
|
|
89
119
|
self,
|
|
@@ -120,6 +150,8 @@ class GitHubAppAuth:
|
|
|
120
150
|
"client_id": self._config.client_id,
|
|
121
151
|
"app_slug": self._config.app_slug,
|
|
122
152
|
"installation_id": self._config.installation_id,
|
|
153
|
+
"installation_ids": dict(self._config.installation_ids),
|
|
154
|
+
"token_broker": self._config.token_broker,
|
|
123
155
|
},
|
|
124
156
|
"status_code": response.status_code,
|
|
125
157
|
"result": response.json() if response.content else {"ok": True},
|
|
@@ -135,7 +167,8 @@ class GitHubAppAuth:
|
|
|
135
167
|
params: dict[str, Any] | None = None,
|
|
136
168
|
) -> dict[str, Any]:
|
|
137
169
|
"""Call the GitHub REST API using the installation token."""
|
|
138
|
-
|
|
170
|
+
repo = repo or repo_from_path(path)
|
|
171
|
+
token = self.get_installation_token(repo=repo)
|
|
139
172
|
url = (
|
|
140
173
|
path if path.startswith("http") else f"{self._config.github_api_url}/{path.lstrip('/')}"
|
|
141
174
|
)
|
|
@@ -158,9 +191,11 @@ class GitHubAppAuth:
|
|
|
158
191
|
"result": parsed,
|
|
159
192
|
}
|
|
160
193
|
|
|
161
|
-
def graphql(
|
|
194
|
+
def graphql(
|
|
195
|
+
self, query: str, variables: dict[str, Any] | None = None, *, repo: str | None = None
|
|
196
|
+
) -> dict[str, Any]:
|
|
162
197
|
"""Call GitHub GraphQL API using the installation token."""
|
|
163
|
-
token = self.get_installation_token()
|
|
198
|
+
token = self.get_installation_token(repo=repo)
|
|
164
199
|
response = self._client.post(
|
|
165
200
|
f"{self._config.github_api_url}/graphql",
|
|
166
201
|
headers={
|
|
@@ -172,7 +207,7 @@ class GitHubAppAuth:
|
|
|
172
207
|
)
|
|
173
208
|
response.raise_for_status()
|
|
174
209
|
return {
|
|
175
|
-
"auth": auth_metadata(token),
|
|
210
|
+
"auth": auth_metadata(token, repo=repo),
|
|
176
211
|
"status_code": response.status_code,
|
|
177
212
|
"result": response.json(),
|
|
178
213
|
}
|
|
@@ -189,6 +224,7 @@ def auth_metadata(token: InstallationToken, *, repo: str | None = None) -> dict[
|
|
|
189
224
|
"actor_expected": actor,
|
|
190
225
|
"repository": repo,
|
|
191
226
|
"token": token.redacted,
|
|
227
|
+
"token_broker": token.token_broker,
|
|
192
228
|
"expires_at": token.expires_at.isoformat(),
|
|
193
229
|
}
|
|
194
230
|
|
|
@@ -203,3 +239,17 @@ def requires_app_jwt(path: str) -> bool:
|
|
|
203
239
|
return False
|
|
204
240
|
normalized = "/" + path_only.lstrip("/")
|
|
205
241
|
return normalized == "/app" or normalized.startswith("/app/")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def repo_from_path(path: str) -> str | None:
|
|
245
|
+
"""Infer OWNER/REPO from common REST API repository paths."""
|
|
246
|
+
path_only = path.split("?", 1)[0]
|
|
247
|
+
if path_only.startswith("http"):
|
|
248
|
+
try:
|
|
249
|
+
path_only = urlparse(path_only).path
|
|
250
|
+
except ValueError:
|
|
251
|
+
return None
|
|
252
|
+
match = re.match(r"^/?repos/([^/]+)/([^/]+)(?:/|$)", path_only)
|
|
253
|
+
if not match:
|
|
254
|
+
return None
|
|
255
|
+
return f"{match.group(1)}/{match.group(2)}"
|
hermes_github_app_plugin/cli.py
CHANGED
|
@@ -16,7 +16,13 @@ from typing import Any, NoReturn
|
|
|
16
16
|
import httpx
|
|
17
17
|
|
|
18
18
|
from .auth import GitHubAppAuth, auth_metadata, requires_app_jwt
|
|
19
|
-
from .config import
|
|
19
|
+
from .config import (
|
|
20
|
+
ConfigurationError,
|
|
21
|
+
GitHubAppConfig,
|
|
22
|
+
load_config,
|
|
23
|
+
parse_token_broker,
|
|
24
|
+
write_github_app_config,
|
|
25
|
+
)
|
|
20
26
|
|
|
21
27
|
|
|
22
28
|
def register_cli(parser: argparse.ArgumentParser) -> None:
|
|
@@ -29,8 +35,21 @@ def register_cli(parser: argparse.ArgumentParser) -> None:
|
|
|
29
35
|
"--non-interactive", action="store_true", help="Read values from flags/env only"
|
|
30
36
|
)
|
|
31
37
|
setup.add_argument("--client-id", help="GitHub App client ID")
|
|
32
|
-
setup.add_argument("--installation-id", help="GitHub App installation ID")
|
|
38
|
+
setup.add_argument("--installation-id", help="Default GitHub App installation ID")
|
|
39
|
+
setup.add_argument(
|
|
40
|
+
"--installation-id-for",
|
|
41
|
+
action="append",
|
|
42
|
+
default=[],
|
|
43
|
+
metavar="OWNER=ID",
|
|
44
|
+
help="Owner-specific installation ID mapping; repeat for multiple orgs",
|
|
45
|
+
)
|
|
33
46
|
setup.add_argument("--private-key-path", help="Path to GitHub App private key PEM")
|
|
47
|
+
setup.add_argument(
|
|
48
|
+
"--token-broker",
|
|
49
|
+
metavar="NAME",
|
|
50
|
+
help="Credentials are injected outside the sandbox by this broker (supported: nono); "
|
|
51
|
+
"no private key is configured",
|
|
52
|
+
)
|
|
34
53
|
setup.add_argument("--app-slug", help="Optional GitHub App slug, e.g. my-agent")
|
|
35
54
|
setup.add_argument(
|
|
36
55
|
"--skip-verify", action="store_true", help="Write config without minting a token"
|
|
@@ -87,9 +106,9 @@ def gh_app_main() -> NoReturn:
|
|
|
87
106
|
print("usage: gh-app [--repo OWNER/REPO] [--] <gh args...>")
|
|
88
107
|
print("Runs gh with GH_TOKEN/GITHUB_TOKEN set to a GitHub App installation token.")
|
|
89
108
|
raise SystemExit(0)
|
|
90
|
-
|
|
109
|
+
repo, child_args = _extract_repo(args)
|
|
91
110
|
config = load_config()
|
|
92
|
-
token = GitHubAppAuth(config).get_installation_token()
|
|
111
|
+
token = GitHubAppAuth(config).get_installation_token(repo=repo)
|
|
93
112
|
env = os.environ.copy()
|
|
94
113
|
env["GH_TOKEN"] = token.token
|
|
95
114
|
env["GITHUB_TOKEN"] = token.token
|
|
@@ -102,9 +121,9 @@ def git_app_main() -> NoReturn:
|
|
|
102
121
|
print("usage: git-app [--repo OWNER/REPO] [--] <git args...>")
|
|
103
122
|
print("Runs git with a temporary askpass helper backed by a GitHub App token.")
|
|
104
123
|
raise SystemExit(0)
|
|
105
|
-
|
|
124
|
+
repo, child_args = _extract_repo(sys.argv[1:])
|
|
106
125
|
config = load_config()
|
|
107
|
-
token = GitHubAppAuth(config).get_installation_token()
|
|
126
|
+
token = GitHubAppAuth(config).get_installation_token(repo=repo)
|
|
108
127
|
with tempfile.TemporaryDirectory(prefix="git-app-") as temp_dir:
|
|
109
128
|
askpass = Path(temp_dir) / "askpass.sh"
|
|
110
129
|
askpass.write_text(
|
|
@@ -127,6 +146,16 @@ def _setup(args: argparse.Namespace) -> int:
|
|
|
127
146
|
"""Interactively write GitHub App configuration."""
|
|
128
147
|
print("Hermes GitHub App setup")
|
|
129
148
|
print("Required values are unmarked. Optional prompts include '(optional)'.")
|
|
149
|
+
token_broker = parse_token_broker(
|
|
150
|
+
getattr(args, "token_broker", None) or os.environ.get("GITHUB_APP_TOKEN_BROKER"),
|
|
151
|
+
source="--token-broker or GITHUB_APP_TOKEN_BROKER",
|
|
152
|
+
)
|
|
153
|
+
if token_broker and args.private_key_path:
|
|
154
|
+
raise ConfigurationError(
|
|
155
|
+
"--private-key-path cannot be combined with --token-broker; "
|
|
156
|
+
"the private key must stay outside the agent's sandbox"
|
|
157
|
+
)
|
|
158
|
+
installation_ids = _parse_installation_id_for_args(list(args.installation_id_for))
|
|
130
159
|
values = {
|
|
131
160
|
"client_id": _value_or_prompt(
|
|
132
161
|
args.client_id,
|
|
@@ -139,10 +168,15 @@ def _setup(args: argparse.Namespace) -> int:
|
|
|
139
168
|
args.installation_id,
|
|
140
169
|
"GitHub App installation ID",
|
|
141
170
|
env="GITHUB_APP_INSTALLATION_ID",
|
|
142
|
-
required=
|
|
171
|
+
required=not installation_ids,
|
|
143
172
|
non_interactive=bool(args.non_interactive),
|
|
144
173
|
),
|
|
145
|
-
"
|
|
174
|
+
"installation_ids": installation_ids,
|
|
175
|
+
"token_broker": token_broker,
|
|
176
|
+
# An empty value removes any private_key_path left by an earlier non-brokered setup.
|
|
177
|
+
"private_key_path": ""
|
|
178
|
+
if token_broker
|
|
179
|
+
else _value_or_prompt(
|
|
146
180
|
args.private_key_path,
|
|
147
181
|
"GitHub App private key path",
|
|
148
182
|
env="GITHUB_APP_PRIVATE_KEY_PATH",
|
|
@@ -157,10 +191,11 @@ def _setup(args: argparse.Namespace) -> int:
|
|
|
157
191
|
non_interactive=bool(args.non_interactive),
|
|
158
192
|
),
|
|
159
193
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
194
|
+
if not token_broker:
|
|
195
|
+
key_path = Path(str(values["private_key_path"])).expanduser()
|
|
196
|
+
if not key_path.exists():
|
|
197
|
+
raise ConfigurationError(f"private key file does not exist: {key_path}")
|
|
198
|
+
_warn_private_key_permissions(key_path)
|
|
164
199
|
written = write_github_app_config(values)
|
|
165
200
|
print(f"Wrote GitHub App config to {written}")
|
|
166
201
|
if args.skip_verify:
|
|
@@ -180,22 +215,19 @@ def _doctor(repo: str | None, *, skip_network: bool) -> int:
|
|
|
180
215
|
try:
|
|
181
216
|
config = load_config()
|
|
182
217
|
checks.append(("GitHub App config loaded", True, "client_id and installation_id present"))
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
checks.append(("private key loaded", True, "inline environment variable"))
|
|
218
|
+
if config.token_broker:
|
|
219
|
+
checks.extend(_token_broker_checks(config.token_broker))
|
|
186
220
|
else:
|
|
187
|
-
checks.
|
|
188
|
-
checks.append(
|
|
189
|
-
(
|
|
190
|
-
"private key file permissions",
|
|
191
|
-
_private_key_permissions_ok(key_source),
|
|
192
|
-
_mode(key_source),
|
|
193
|
-
)
|
|
194
|
-
)
|
|
221
|
+
checks.extend(_private_key_checks(config))
|
|
195
222
|
if not skip_network:
|
|
196
223
|
auth = GitHubAppAuth(config)
|
|
197
|
-
token = auth.get_installation_token(force_refresh=True)
|
|
198
|
-
|
|
224
|
+
token = auth.get_installation_token(repo=repo, force_refresh=True)
|
|
225
|
+
minted_label = (
|
|
226
|
+
f"installation token brokered by {config.token_broker}"
|
|
227
|
+
if config.token_broker
|
|
228
|
+
else "installation token minted"
|
|
229
|
+
)
|
|
230
|
+
checks.append((minted_label, True, token.redacted))
|
|
199
231
|
app_result = auth.app_request("GET", "/app")["result"]
|
|
200
232
|
checks.append(("/app API reachable", True, str(app_result.get("slug", "ok"))))
|
|
201
233
|
if repo:
|
|
@@ -221,10 +253,39 @@ def _doctor(repo: str | None, *, skip_network: bool) -> int:
|
|
|
221
253
|
return 1
|
|
222
254
|
|
|
223
255
|
|
|
256
|
+
def _private_key_checks(config: GitHubAppConfig) -> list[tuple[str, bool, str]]:
|
|
257
|
+
if config.private_key_source == "GITHUB_APP_PRIVATE_KEY":
|
|
258
|
+
return [("private key loaded", True, "inline environment variable")]
|
|
259
|
+
key_source = Path(config.private_key_source).expanduser()
|
|
260
|
+
return [
|
|
261
|
+
("private key file exists", key_source.exists(), str(key_source)),
|
|
262
|
+
(
|
|
263
|
+
"private key file permissions",
|
|
264
|
+
_private_key_permissions_ok(key_source),
|
|
265
|
+
_mode(key_source),
|
|
266
|
+
),
|
|
267
|
+
]
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _token_broker_checks(broker: str) -> list[tuple[str, bool, str]]:
|
|
271
|
+
proxy_set = bool(os.environ.get("HTTPS_PROXY") or os.environ.get("https_proxy"))
|
|
272
|
+
return [
|
|
273
|
+
("token broker", True, f"{broker}; credentials are injected outside the sandbox"),
|
|
274
|
+
(
|
|
275
|
+
"HTTPS_PROXY set",
|
|
276
|
+
proxy_set,
|
|
277
|
+
"set"
|
|
278
|
+
if proxy_set
|
|
279
|
+
else "not set; agent does not appear to be running inside nono, "
|
|
280
|
+
"so no credentials will be injected",
|
|
281
|
+
),
|
|
282
|
+
]
|
|
283
|
+
|
|
284
|
+
|
|
224
285
|
def _status(repo: str | None) -> int:
|
|
225
286
|
config = load_config()
|
|
226
287
|
auth = GitHubAppAuth(config)
|
|
227
|
-
token = auth.get_installation_token(force_refresh=True)
|
|
288
|
+
token = auth.get_installation_token(repo=repo, force_refresh=True)
|
|
228
289
|
app = auth.app_request("GET", "/app")["result"]
|
|
229
290
|
repo_probe = auth.request("GET", f"/repos/{repo}", repo=repo)["result"] if repo else None
|
|
230
291
|
print(
|
|
@@ -244,11 +305,17 @@ def _status(repo: str | None) -> int:
|
|
|
244
305
|
|
|
245
306
|
def _token(repo: str | None, *, json_output: bool) -> int:
|
|
246
307
|
config = load_config()
|
|
247
|
-
token = GitHubAppAuth(config).get_installation_token()
|
|
308
|
+
token = GitHubAppAuth(config).get_installation_token(repo=repo)
|
|
248
309
|
if json_output:
|
|
249
310
|
print(json.dumps({"token": token.token, "auth": auth_metadata(token, repo=repo)}, indent=2))
|
|
250
311
|
else:
|
|
251
312
|
print(token.token)
|
|
313
|
+
if config.token_broker:
|
|
314
|
+
print(
|
|
315
|
+
"note: token broker mode. The printed value is a placeholder; the real installation "
|
|
316
|
+
"token lives outside the sandbox and the nono proxy injects it into GitHub requests.",
|
|
317
|
+
file=sys.stderr,
|
|
318
|
+
)
|
|
252
319
|
return 0
|
|
253
320
|
|
|
254
321
|
|
|
@@ -262,6 +329,25 @@ def _api(method: str, path: str, *, repo: str | None, body: dict[str, Any] | Non
|
|
|
262
329
|
return 0
|
|
263
330
|
|
|
264
331
|
|
|
332
|
+
def _parse_installation_id_for_args(values: list[str]) -> dict[str, str]:
|
|
333
|
+
"""Parse repeated OWNER=INSTALLATION_ID setup flags."""
|
|
334
|
+
installation_ids: dict[str, str] = {}
|
|
335
|
+
for value in values:
|
|
336
|
+
if "=" not in value:
|
|
337
|
+
raise ConfigurationError(
|
|
338
|
+
f"invalid --installation-id-for value: {value!r}; expected OWNER=INSTALLATION_ID"
|
|
339
|
+
)
|
|
340
|
+
owner, installation_id = value.split("=", 1)
|
|
341
|
+
owner = owner.strip().lower()
|
|
342
|
+
installation_id = installation_id.strip()
|
|
343
|
+
if not owner or not installation_id:
|
|
344
|
+
raise ConfigurationError(
|
|
345
|
+
f"invalid --installation-id-for value: {value!r}; expected OWNER=INSTALLATION_ID"
|
|
346
|
+
)
|
|
347
|
+
installation_ids[owner] = installation_id
|
|
348
|
+
return installation_ids
|
|
349
|
+
|
|
350
|
+
|
|
265
351
|
def _value_or_prompt(
|
|
266
352
|
value: str | None,
|
|
267
353
|
label: str,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import json
|
|
5
6
|
import os
|
|
6
7
|
from collections.abc import Mapping
|
|
7
8
|
from dataclasses import dataclass
|
|
@@ -10,6 +11,11 @@ from typing import Any
|
|
|
10
11
|
|
|
11
12
|
import yaml
|
|
12
13
|
|
|
14
|
+
# Token brokers keep the private key outside the agent's sandbox. The broker injects real
|
|
15
|
+
# credentials into outbound requests, so the agent only ever sends this placeholder.
|
|
16
|
+
SUPPORTED_TOKEN_BROKERS: tuple[str, ...] = ("nono",)
|
|
17
|
+
NONO_PHANTOM_TOKEN = "nono-phantom-github-app"
|
|
18
|
+
|
|
13
19
|
|
|
14
20
|
class ConfigurationError(RuntimeError):
|
|
15
21
|
"""Raised when GitHub App configuration is missing or invalid."""
|
|
@@ -17,14 +23,28 @@ class ConfigurationError(RuntimeError):
|
|
|
17
23
|
|
|
18
24
|
@dataclass(frozen=True)
|
|
19
25
|
class GitHubAppConfig:
|
|
20
|
-
"""Per-agent GitHub App configuration.
|
|
26
|
+
"""Per-agent GitHub App configuration.
|
|
27
|
+
|
|
28
|
+
In token broker mode `private_key` is empty and `token_broker` names the broker.
|
|
29
|
+
"""
|
|
21
30
|
|
|
22
31
|
client_id: str
|
|
23
32
|
installation_id: str
|
|
24
33
|
private_key: str
|
|
34
|
+
installation_ids: dict[str, str]
|
|
25
35
|
private_key_source: str
|
|
26
36
|
app_slug: str | None = None
|
|
27
37
|
github_api_url: str = "https://api.github.com"
|
|
38
|
+
token_broker: str | None = None
|
|
39
|
+
|
|
40
|
+
def installation_id_for_repo(self, repo: str | None = None) -> str:
|
|
41
|
+
"""Return the installation ID for an optional OWNER/REPO target."""
|
|
42
|
+
if repo and "/" in repo:
|
|
43
|
+
owner = repo.split("/", 1)[0].lower()
|
|
44
|
+
installation_id = self.installation_ids.get(owner)
|
|
45
|
+
if installation_id:
|
|
46
|
+
return installation_id
|
|
47
|
+
return self.installation_id
|
|
28
48
|
|
|
29
49
|
|
|
30
50
|
def hermes_home() -> Path:
|
|
@@ -63,7 +83,7 @@ def write_github_app_config(values: Mapping[str, Any]) -> Path:
|
|
|
63
83
|
section = {}
|
|
64
84
|
data["github_app"] = section
|
|
65
85
|
for key, value in values.items():
|
|
66
|
-
if value in (None, "", (), []):
|
|
86
|
+
if value in (None, "", (), []) or (isinstance(value, Mapping) and not value):
|
|
67
87
|
section.pop(key, None)
|
|
68
88
|
else:
|
|
69
89
|
section[key] = value
|
|
@@ -76,6 +96,52 @@ def write_github_app_config(values: Mapping[str, Any]) -> Path:
|
|
|
76
96
|
return path
|
|
77
97
|
|
|
78
98
|
|
|
99
|
+
def _parse_installation_ids(raw: Any) -> dict[str, str]:
|
|
100
|
+
"""Parse owner-to-installation-ID mapping from YAML or environment values."""
|
|
101
|
+
if raw in (None, ""):
|
|
102
|
+
return {}
|
|
103
|
+
parsed: Any = raw
|
|
104
|
+
if isinstance(raw, str):
|
|
105
|
+
try:
|
|
106
|
+
parsed = json.loads(raw)
|
|
107
|
+
except json.JSONDecodeError:
|
|
108
|
+
parsed = {}
|
|
109
|
+
for item in raw.replace("\n", ",").split(","):
|
|
110
|
+
if not item.strip():
|
|
111
|
+
continue
|
|
112
|
+
if "=" not in item:
|
|
113
|
+
raise ConfigurationError(
|
|
114
|
+
"invalid GITHUB_APP_INSTALLATION_IDS entry: "
|
|
115
|
+
f"{item!r}; expected OWNER=INSTALLATION_ID"
|
|
116
|
+
) from None
|
|
117
|
+
owner, installation_id = item.split("=", 1)
|
|
118
|
+
parsed[owner.strip()] = installation_id.strip()
|
|
119
|
+
if not isinstance(parsed, Mapping):
|
|
120
|
+
raise ConfigurationError("github_app.installation_ids must be a mapping of owner to ID")
|
|
121
|
+
result: dict[str, str] = {}
|
|
122
|
+
for owner, installation_id in parsed.items():
|
|
123
|
+
owner_text = str(owner).strip().lower()
|
|
124
|
+
installation_text = str(installation_id).strip()
|
|
125
|
+
if owner_text and installation_text:
|
|
126
|
+
result[owner_text] = installation_text
|
|
127
|
+
return result
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def parse_token_broker(raw: object, *, source: str) -> str | None:
|
|
131
|
+
"""Normalize a token broker name; empty values mean no broker."""
|
|
132
|
+
if raw is None:
|
|
133
|
+
return None
|
|
134
|
+
text = str(raw).strip().lower()
|
|
135
|
+
if not text:
|
|
136
|
+
return None
|
|
137
|
+
if text not in SUPPORTED_TOKEN_BROKERS:
|
|
138
|
+
supported = ", ".join(SUPPORTED_TOKEN_BROKERS)
|
|
139
|
+
raise ConfigurationError(
|
|
140
|
+
f"unsupported token broker {str(raw)!r} from {source}; supported: {supported}"
|
|
141
|
+
)
|
|
142
|
+
return text
|
|
143
|
+
|
|
144
|
+
|
|
79
145
|
def _read_private_key(section: Mapping[str, Any]) -> tuple[str, str]:
|
|
80
146
|
inline_key = os.environ.get("GITHUB_APP_PRIVATE_KEY") or str(section.get("private_key", ""))
|
|
81
147
|
if inline_key:
|
|
@@ -106,18 +172,35 @@ def load_config() -> GitHubAppConfig:
|
|
|
106
172
|
raise ConfigurationError(
|
|
107
173
|
"missing GitHub App client ID: set GITHUB_APP_CLIENT_ID or github_app.client_id"
|
|
108
174
|
)
|
|
109
|
-
|
|
175
|
+
installation_ids = _parse_installation_ids(
|
|
176
|
+
os.environ.get("GITHUB_APP_INSTALLATION_IDS") or section.get("installation_ids", {})
|
|
177
|
+
)
|
|
178
|
+
if not installation_id and not installation_ids:
|
|
110
179
|
raise ConfigurationError(
|
|
111
|
-
"missing GitHub App installation ID: set GITHUB_APP_INSTALLATION_ID "
|
|
112
|
-
"or github_app.
|
|
180
|
+
"missing GitHub App installation ID: set GITHUB_APP_INSTALLATION_ID, "
|
|
181
|
+
"github_app.installation_id, or github_app.installation_ids"
|
|
113
182
|
)
|
|
114
|
-
|
|
183
|
+
if not installation_id:
|
|
184
|
+
# Use a deterministic fallback for commands that cannot infer an OWNER/REPO.
|
|
185
|
+
installation_id = next(iter(installation_ids.values()))
|
|
186
|
+
broker_env = os.environ.get("GITHUB_APP_TOKEN_BROKER")
|
|
187
|
+
token_broker = parse_token_broker(
|
|
188
|
+
broker_env or section.get("token_broker"),
|
|
189
|
+
source="GITHUB_APP_TOKEN_BROKER" if broker_env else "github_app.token_broker",
|
|
190
|
+
)
|
|
191
|
+
if token_broker:
|
|
192
|
+
# The private key never enters a brokered agent's environment; ignore any key settings.
|
|
193
|
+
private_key, private_key_source = "", f"token broker: {token_broker}"
|
|
194
|
+
else:
|
|
195
|
+
private_key, private_key_source = _read_private_key(section)
|
|
115
196
|
return GitHubAppConfig(
|
|
116
197
|
client_id=client_id,
|
|
117
198
|
installation_id=installation_id,
|
|
118
199
|
private_key=private_key,
|
|
200
|
+
installation_ids=installation_ids,
|
|
119
201
|
private_key_source=private_key_source,
|
|
120
202
|
app_slug=os.environ.get("GITHUB_APP_SLUG") or section.get("app_slug"),
|
|
121
203
|
github_api_url=os.environ.get("GITHUB_API_URL")
|
|
122
204
|
or str(section.get("github_api_url", "https://api.github.com")).rstrip("/"),
|
|
205
|
+
token_broker=token_broker,
|
|
123
206
|
)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
name: github-app
|
|
2
|
-
version: 0.
|
|
2
|
+
version: 0.3.0
|
|
3
3
|
description: "GitHub App identity integration for Hermes: per-agent app token minting, GitHub API tools, gh/git wrappers, CLI, and workflow skill."
|
|
4
4
|
author: Hermes GitHub App Plugin Contributors
|
|
5
5
|
provides_tools:
|
|
@@ -20,8 +20,14 @@ requires_env:
|
|
|
20
20
|
description: "GitHub App client ID for this Hermes agent. Can also be set in ~/.hermes/config.yaml under github_app.client_id."
|
|
21
21
|
secret: false
|
|
22
22
|
- name: GITHUB_APP_INSTALLATION_ID
|
|
23
|
-
description: "GitHub App installation ID for this Hermes agent. Can also be set in ~/.hermes/config.yaml under github_app.installation_id."
|
|
23
|
+
description: "Default GitHub App installation ID for this Hermes agent. Can also be set in ~/.hermes/config.yaml under github_app.installation_id."
|
|
24
|
+
secret: false
|
|
25
|
+
- name: GITHUB_APP_INSTALLATION_IDS
|
|
26
|
+
description: "Optional owner-to-installation-ID mapping for apps installed in multiple orgs. JSON object or OWNER=ID,OWNER2=ID2. Can also be set under github_app.installation_ids."
|
|
24
27
|
secret: false
|
|
25
28
|
- name: GITHUB_APP_PRIVATE_KEY_PATH
|
|
26
|
-
description: "Path to the GitHub App private key PEM. Can also be set in ~/.hermes/config.yaml under github_app.private_key_path."
|
|
29
|
+
description: "Path to the GitHub App private key PEM. Can also be set in ~/.hermes/config.yaml under github_app.private_key_path. Not used when GITHUB_APP_TOKEN_BROKER is set."
|
|
27
30
|
secret: true
|
|
31
|
+
- name: GITHUB_APP_TOKEN_BROKER
|
|
32
|
+
description: "Set to nono when the agent runs in the nono sandbox and the token broker injects credentials. No private key is read in this mode. Can also be set in ~/.hermes/config.yaml under github_app.token_broker."
|
|
33
|
+
secret: false
|
|
@@ -11,7 +11,17 @@ Use this skill for GitHub operations from Hermes agents that are configured with
|
|
|
11
11
|
|
|
12
12
|
## First-time setup
|
|
13
13
|
|
|
14
|
-
Run `hermes-github-app setup` to write `github_app` config into `~/.hermes/config.yaml`. The setup walkthrough marks optional values with `(optional)`; the required values are GitHub App client ID, installation ID
|
|
14
|
+
Run `hermes-github-app setup` to write `github_app` config into `~/.hermes/config.yaml`. The setup walkthrough marks optional values with `(optional)`; the required values are GitHub App client ID, a default installation ID or owner-specific `installation_ids`, and private key path. For apps installed in multiple organizations, pass repeated `--installation-id-for OWNER=ID` flags or configure:
|
|
15
|
+
|
|
16
|
+
```yaml
|
|
17
|
+
github_app:
|
|
18
|
+
installation_id: "123456" # default/fallback
|
|
19
|
+
installation_ids:
|
|
20
|
+
ExampleOrg: "123456"
|
|
21
|
+
ExampleInfra: "789012"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The plugin chooses an owner-specific installation ID from `--repo OWNER/REPO` or `/repos/OWNER/REPO` REST API paths and falls back to `installation_id` when no repository owner is available.
|
|
15
25
|
|
|
16
26
|
After setup, run `hermes-github-app doctor --repo OWNER/REPO` to verify console scripts, config loading, private-key permissions, token minting, and repository access. Use `--skip-network` only for container/image builds where secrets or network access are not available yet.
|
|
17
27
|
|
|
@@ -26,6 +36,14 @@ After setup, run `hermes-github-app doctor --repo OWNER/REPO` to verify console
|
|
|
26
36
|
7. Expect comments, PRs, and API writes to appear as `<app-slug>[bot]`.
|
|
27
37
|
8. Treat the GitHub App installation scope as the repository access boundary. The plugin does not maintain a separate local repository/owner allowlist.
|
|
28
38
|
|
|
39
|
+
## Running inside nono (token broker mode)
|
|
40
|
+
|
|
41
|
+
If `hermes-github-app doctor` reports `token broker: nono`, this agent runs in the nono sandbox. By design it holds no GitHub App private key and cannot obtain a real installation token.
|
|
42
|
+
|
|
43
|
+
- Do not search for, copy, or request a private key or a real token. `hermes-github-app token` prints a placeholder; nono's proxy outside the sandbox injects the real credentials into GitHub requests.
|
|
44
|
+
- `github_app_*` tools, `gh-app`, and `git-app` work as described above. Plain `gh` and HTTPS `git push` also work because the proxy injects credentials, so keep remotes HTTPS.
|
|
45
|
+
- If `doctor` reports `HTTPS_PROXY set` as failed, the agent is not running inside nono and no credentials will be injected. Stop and report that. Do not fall back to local human credentials or SSH keys.
|
|
46
|
+
|
|
29
47
|
## Updating existing Hermes skills
|
|
30
48
|
|
|
31
49
|
Patch any GitHub-related skill, cron prompt, or subagent instruction that mentions `gh`, `git push`, GitHub SSH remotes, or `@me` assumptions:
|
|
@@ -39,8 +39,10 @@ def github_app_status(params: dict[str, Any], **_: Any) -> str:
|
|
|
39
39
|
"configured": True,
|
|
40
40
|
"client_id": config.client_id,
|
|
41
41
|
"installation_id": config.installation_id,
|
|
42
|
+
"installation_ids": dict(config.installation_ids),
|
|
42
43
|
"app_slug": config.app_slug,
|
|
43
44
|
"private_key_source": config.private_key_source,
|
|
45
|
+
"token_broker": config.token_broker,
|
|
44
46
|
"github_api_url": config.github_api_url,
|
|
45
47
|
"scope_management": "github_app_installation",
|
|
46
48
|
}
|
|
@@ -54,7 +56,7 @@ def github_app_verify_identity(params: dict[str, Any], **_: Any) -> str:
|
|
|
54
56
|
def run() -> dict[str, Any]:
|
|
55
57
|
repo = _repo(params)
|
|
56
58
|
auth = _auth()
|
|
57
|
-
token = auth.get_installation_token(force_refresh=True)
|
|
59
|
+
token = auth.get_installation_token(repo=repo, force_refresh=True)
|
|
58
60
|
app = auth.app_request("GET", "/app")
|
|
59
61
|
repo_probe = auth.request("GET", f"/repos/{repo}", repo=repo) if repo else None
|
|
60
62
|
return {
|
|
@@ -92,7 +94,9 @@ def github_app_graphql(params: dict[str, Any], **_: Any) -> str:
|
|
|
92
94
|
def run() -> dict[str, Any]:
|
|
93
95
|
variables = params.get("variables")
|
|
94
96
|
return _auth().graphql(
|
|
95
|
-
str(params["query"]),
|
|
97
|
+
str(params["query"]),
|
|
98
|
+
variables if isinstance(variables, dict) else None,
|
|
99
|
+
repo=_repo(params),
|
|
96
100
|
)
|
|
97
101
|
|
|
98
102
|
return _handle_errors(run)
|
{hermes_github_app_plugin-0.1.2.dist-info → hermes_github_app_plugin-0.3.0.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: hermes-github-app-plugin
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Hermes plugin for per-agent GitHub App identity, gh/git wrappers, and GitHub App-aware tools.
|
|
5
5
|
Author: Hermes GitHub App Plugin Contributors
|
|
6
6
|
License: MIT
|
|
@@ -26,7 +26,12 @@ Each Hermes agent runs the same package but is configured with its own GitHub Ap
|
|
|
26
26
|
```yaml
|
|
27
27
|
github_app:
|
|
28
28
|
client_id: "Iv1.exampleclientid"
|
|
29
|
+
# Optional default used when a command cannot infer OWNER/REPO.
|
|
29
30
|
installation_id: "987654"
|
|
31
|
+
# Optional owner-specific installation IDs for apps installed in multiple orgs.
|
|
32
|
+
installation_ids:
|
|
33
|
+
ExampleOrg: "123456"
|
|
34
|
+
ExampleInfra: "987654"
|
|
30
35
|
private_key_path: "~/.hermes/secrets/agent-github-app.private-key.pem"
|
|
31
36
|
app_slug: "hermes-agent"
|
|
32
37
|
```
|
|
@@ -35,8 +40,10 @@ Environment variables with the same meaning are also supported:
|
|
|
35
40
|
|
|
36
41
|
- `GITHUB_APP_CLIENT_ID`
|
|
37
42
|
- `GITHUB_APP_INSTALLATION_ID`
|
|
43
|
+
- `GITHUB_APP_INSTALLATION_IDS` (JSON object or `OWNER=ID,OWNER2=ID2` mapping)
|
|
38
44
|
- `GITHUB_APP_PRIVATE_KEY_PATH`
|
|
39
45
|
- `GITHUB_APP_PRIVATE_KEY` (PEM contents; useful for CI)
|
|
46
|
+
- `GITHUB_APP_TOKEN_BROKER=nono` (no private key in this environment; see [Running inside nono](#running-inside-nono-token-broker-mode))
|
|
40
47
|
|
|
41
48
|
Repository access is controlled by the GitHub App installation scope in GitHub. If an agent should not access a repository, remove that repository from the GitHub App installation scope.
|
|
42
49
|
|
|
@@ -46,7 +53,7 @@ Repository access is controlled by the GitHub App installation scope in GitHub.
|
|
|
46
53
|
|
|
47
54
|
`installation_id` identifies one installation of that app on a specific user or organization account. It is required when exchanging the app JWT for an installation access token via `POST /app/installations/{installation_id}/access_tokens`.
|
|
48
55
|
|
|
49
|
-
In other words: `client_id` answers "which GitHub App is signing this JWT?" while `installation_id` answers "which installed copy of that app should this token act as?" The same GitHub App can have multiple installation IDs if it is installed on multiple accounts.
|
|
56
|
+
In other words: `client_id` answers "which GitHub App is signing this JWT?" while `installation_id` answers "which installed copy of that app should this token act as?" The same GitHub App can have multiple installation IDs if it is installed on multiple accounts. Configure `installation_ids` when one app is installed in multiple organizations; the plugin chooses an owner-specific ID from `--repo OWNER/REPO` or `/repos/OWNER/REPO` REST paths and falls back to `installation_id` when no repository owner is available.
|
|
50
57
|
|
|
51
58
|
## Install
|
|
52
59
|
|
|
@@ -72,6 +79,8 @@ For scripted installs, pass flags and skip the network verification until secret
|
|
|
72
79
|
hermes-github-app setup --non-interactive --skip-verify \
|
|
73
80
|
--client-id Iv1.exampleclientid \
|
|
74
81
|
--installation-id 987654 \
|
|
82
|
+
--installation-id-for ExampleOrg=123456 \
|
|
83
|
+
--installation-id-for ExampleInfra=987654 \
|
|
75
84
|
--private-key-path ~/.hermes/secrets/agent-github-app.private-key.pem \
|
|
76
85
|
--app-slug hermes-agent
|
|
77
86
|
```
|
|
@@ -94,6 +103,35 @@ git-app --repo OWNER/REPO push origin my-branch
|
|
|
94
103
|
`gh-app` injects an ephemeral installation token as `GH_TOKEN` and `GITHUB_TOKEN` for the child `gh` process.
|
|
95
104
|
`git-app` injects a temporary askpass helper so HTTPS Git operations authenticate as the GitHub App installation token without writing credentials into the remote URL.
|
|
96
105
|
|
|
106
|
+
## Running inside nono (token broker mode)
|
|
107
|
+
|
|
108
|
+
[nono](https://nono.sh) can run the agent in a sandbox that never holds the GitHub App private key. Enable broker mode with `GITHUB_APP_TOKEN_BROKER=nono` or `github_app.token_broker: nono`:
|
|
109
|
+
|
|
110
|
+
```yaml
|
|
111
|
+
github_app:
|
|
112
|
+
client_id: "Iv1.exampleclientid"
|
|
113
|
+
installation_ids:
|
|
114
|
+
ExampleOrg: "123456"
|
|
115
|
+
token_broker: nono
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
hermes-github-app setup --token-broker nono --client-id Iv1.exampleclientid --installation-id 987654
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
In broker mode the plugin reads no private key; `private_key_path` and `GITHUB_APP_PRIVATE_KEY*` are ignored. Installation tokens are not minted locally. The plugin sends the placeholder `nono-phantom-github-app` as its token, and nono's supervisor, outside the sandbox, handles the real credentials:
|
|
123
|
+
|
|
124
|
+
- Its TLS-intercepting proxy replaces the `Authorization` header on requests to `api.github.com`, `uploads.github.com`, and `github.com` with an installation token chosen by owner from the request path. Git HTTPS uses the same token as Basic `x-access-token`.
|
|
125
|
+
- The app JWT is injected for exactly `GET /app`.
|
|
126
|
+
|
|
127
|
+
What this means for agents:
|
|
128
|
+
|
|
129
|
+
- `github_app_*` tools, `hermes-github-app api`, `gh-app`, and `git-app` work unchanged; the placeholder flows through.
|
|
130
|
+
- Plain `git push` over HTTPS and plain `gh` also work, because the proxy injects credentials into those requests. Keep remotes HTTPS. SSH remotes use local SSH keys and are not covered.
|
|
131
|
+
- The agent cannot obtain a real token, by design. `hermes-github-app token` prints the placeholder with a note on stderr.
|
|
132
|
+
- `client_id` is still required in broker mode. It is non-secret and is reported in auth metadata.
|
|
133
|
+
- `hermes-github-app doctor` shows a `token broker` check and an `HTTPS_PROXY set` check. If the proxy variable is missing, the agent is not running inside nono and no credentials will be injected.
|
|
134
|
+
|
|
97
135
|
## Migrating existing Hermes skills and jobs
|
|
98
136
|
|
|
99
137
|
To keep agents from falling back to local human credentials, update existing GitHub-related Hermes skills, cron jobs, and subagent prompts with these rules:
|
|
@@ -127,8 +165,8 @@ Before the first release, configure PyPI Trusted Publishing for this repository
|
|
|
127
165
|
Release example:
|
|
128
166
|
|
|
129
167
|
```bash
|
|
130
|
-
git tag 0.
|
|
131
|
-
git push origin 0.
|
|
168
|
+
git tag 0.3.0
|
|
169
|
+
git push origin 0.3.0
|
|
132
170
|
```
|
|
133
171
|
|
|
134
172
|
Tags like `v0.1.0`, `0.1`, or `0.1.0rc1` will not publish.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
hermes_github_app_plugin/__init__.py,sha256=NnRzmqrrdX7lgoqhFn8LXVyo7pCzAT9Hyx9U452RIvc,1956
|
|
2
|
+
hermes_github_app_plugin/auth.py,sha256=QRvLk3aqWxWrtYrMIVuxsKZLPwlINf-qlWcD3g6v0E8,9183
|
|
3
|
+
hermes_github_app_plugin/cli.py,sha256=qOkahrMpK_6JktDBEzWDjBL88X5vHx7tjOgb5DgmaVI,15707
|
|
4
|
+
hermes_github_app_plugin/config.py,sha256=fdnHmsI15li387Y8DOx8KTQlWXlpEPJ8V9tO-TwDpSA,7813
|
|
5
|
+
hermes_github_app_plugin/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
hermes_github_app_plugin/schemas.py,sha256=yNDBOEvXzhDRtMWfX8mGmdMyAFtP_d9H4_j40C_BqTo,3099
|
|
7
|
+
hermes_github_app_plugin/tools.py,sha256=_H439zRIYDcExbzepl1OiTtw1N4Q2SExK6BN3myexGA,5489
|
|
8
|
+
hermes_github_app_plugin/skills/github-app-workflow/SKILL.md,sha256=rteb4bR3YcA8VMpIjGARsqcw6Bbv0hVz-5YniazOvpA,4314
|
|
9
|
+
hermes_github_app_plugin/plugin.yaml,sha256=_WEbl9TlTQHSi9yOOz5a9xJlFeFUlGOvQEYq06Y4no4,1657
|
|
10
|
+
hermes_github_app_plugin-0.3.0.dist-info/METADATA,sha256=m3W7Z4H5ZzT-I4EeXtpeFgYDStwq83bwlv3a0ULudfM,8413
|
|
11
|
+
hermes_github_app_plugin-0.3.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
12
|
+
hermes_github_app_plugin-0.3.0.dist-info/entry_points.txt,sha256=ydr4f-i9GywzITOEUQovI8QkquJwew8NrHbOsE5qrYQ,236
|
|
13
|
+
hermes_github_app_plugin-0.3.0.dist-info/licenses/LICENSE,sha256=6zZgOs9Rdg8m9nRz_a-8VmYPNTVOA45JCqXUfpUYmdE,1094
|
|
14
|
+
hermes_github_app_plugin-0.3.0.dist-info/RECORD,,
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
hermes_github_app_plugin/__init__.py,sha256=NnRzmqrrdX7lgoqhFn8LXVyo7pCzAT9Hyx9U452RIvc,1956
|
|
2
|
-
hermes_github_app_plugin/auth.py,sha256=vJ6mzKINoMu6QawVy9V_8G4RWEreGY-jpNODuHw3374,7131
|
|
3
|
-
hermes_github_app_plugin/cli.py,sha256=ll3sKLO_VZWmBRdTj0d64X3QgswzC4V-f00vtm_QiZw,12443
|
|
4
|
-
hermes_github_app_plugin/config.py,sha256=m0s7IDGnnchndhjsoPzMJAONTYk8SLcZjNi0K1qiaQM,4217
|
|
5
|
-
hermes_github_app_plugin/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
-
hermes_github_app_plugin/schemas.py,sha256=yNDBOEvXzhDRtMWfX8mGmdMyAFtP_d9H4_j40C_BqTo,3099
|
|
7
|
-
hermes_github_app_plugin/tools.py,sha256=Rgq1vbub5YHUYJ92h2KDDjxQxDjbWschckC0dnGh9W0,5321
|
|
8
|
-
hermes_github_app_plugin/skills/github-app-workflow/SKILL.md,sha256=2W2-Hp5xfR8ZyxhgBmJyP1SJmkLStP9wbNdCVsw68YI,3002
|
|
9
|
-
hermes_github_app_plugin/plugin.yaml,sha256=1bYCxwR5wVbaWWRh3mHx5Wd8ghTZIbWIyqDzhMgo09E,1088
|
|
10
|
-
hermes_github_app_plugin-0.1.2.dist-info/METADATA,sha256=PzmyeW8DZEdmdexnP_wL_Ee-OYK0yCrnKheCtGcxm5o,5785
|
|
11
|
-
hermes_github_app_plugin-0.1.2.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
12
|
-
hermes_github_app_plugin-0.1.2.dist-info/entry_points.txt,sha256=ydr4f-i9GywzITOEUQovI8QkquJwew8NrHbOsE5qrYQ,236
|
|
13
|
-
hermes_github_app_plugin-0.1.2.dist-info/licenses/LICENSE,sha256=6zZgOs9Rdg8m9nRz_a-8VmYPNTVOA45JCqXUfpUYmdE,1094
|
|
14
|
-
hermes_github_app_plugin-0.1.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|