Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,8 @@ Before the first release, configure PyPI Trusted Publishing for this repository
Release example:

```bash
git tag 0.1.1
git push origin 0.1.1
git tag 0.1.2
git push origin 0.1.2
```

Tags like `v0.1.0`, `0.1`, or `0.1.0rc1` will not publish.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "hermes-github-app-plugin"
version = "0.1.1"
version = "0.1.2"
description = "Hermes plugin for per-agent GitHub App identity, gh/git wrappers, and GitHub App-aware tools."
readme = "README.md"
requires-python = ">=3.10"
Expand Down
40 changes: 40 additions & 0 deletions src/hermes_github_app_plugin/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,46 @@ def get_installation_token(self, *, force_refresh: bool = False) -> Installation
self._cached_token = token
return token

def app_request(
self,
method: str,
path: str,
*,
json_body: dict[str, Any] | None = None,
params: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Call a GitHub App endpoint using the app JWT.

Endpoints like `GET /app` authenticate as the GitHub App itself and
reject installation access tokens. Repository and user endpoints should
continue to use `request()`, which authenticates as the installation.
"""
url = (
path if path.startswith("http") else f"{self._config.github_api_url}/{path.lstrip('/')}"
)
response = self._client.request(
method.upper(),
url,
headers={
"Accept": "application/vnd.github+json",
"Authorization": f"Bearer {self.create_jwt()}",
"X-GitHub-Api-Version": "2022-11-28",
},
json=json_body,
params=params,
)
response.raise_for_status()
return {
"auth": {
"auth_mode": "github_app_jwt",
"client_id": self._config.client_id,
"app_slug": self._config.app_slug,
"installation_id": self._config.installation_id,
},
"status_code": response.status_code,
"result": response.json() if response.content else {"ok": True},
}

def request(
self,
method: str,
Expand Down
4 changes: 2 additions & 2 deletions src/hermes_github_app_plugin/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ def _doctor(repo: str | None, *, skip_network: bool) -> int:
auth = GitHubAppAuth(config)
token = auth.get_installation_token(force_refresh=True)
checks.append(("installation token minted", True, token.redacted))
app_result = auth.request("GET", "/app")["result"]
app_result = auth.app_request("GET", "/app")["result"]
checks.append(("/app API reachable", True, str(app_result.get("slug", "ok"))))
if repo:
repo_result = auth.request("GET", f"/repos/{repo}", repo=repo)["result"]
Expand Down Expand Up @@ -225,7 +225,7 @@ def _status(repo: str | None) -> int:
config = load_config()
auth = GitHubAppAuth(config)
token = auth.get_installation_token(force_refresh=True)
app = auth.request("GET", "/app")["result"]
app = auth.app_request("GET", "/app")["result"]
repo_probe = auth.request("GET", f"/repos/{repo}", repo=repo)["result"] if repo else None
print(
json.dumps(
Expand Down
25 changes: 25 additions & 0 deletions tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,28 @@ def handler(request: httpx.Request) -> httpx.Response:
assert seen[0].headers["authorization"] == "Bearer jwt-token"
assert seen[1].headers["authorization"] == "Bearer ghu_installation_token"
assert json.loads(json.dumps(result))["auth"]["auth_mode"] == "github_app"


def test_app_request_uses_app_jwt(monkeypatch) -> None: # type: ignore[no-untyped-def]
monkeypatch.setattr("jwt.encode", lambda *_, **__: "jwt-token")
seen: list[httpx.Request] = []

def handler(request: httpx.Request) -> httpx.Response:
seen.append(request)
return httpx.Response(200, json={"slug": "hermes-test-agent"})

client = httpx.Client(transport=httpx.MockTransport(handler))
config = GitHubAppConfig(
client_id="123",
installation_id="456",
private_key=PRIVATE_KEY,
private_key_source="env",
app_slug="hermes-test-agent",
)

result = GitHubAppAuth(config, client=client).app_request("GET", "/app")

assert result["result"] == {"slug": "hermes-test-agent"}
assert seen[0].url.path == "/app"
assert seen[0].headers["authorization"] == "Bearer jwt-token"
assert result["auth"]["auth_mode"] == "github_app_jwt"
Loading