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
8 changes: 4 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ repos:
rev: v6.0.0
hooks:
- id: check-yaml
args: ['--unsafe']
args: ["--unsafe"]
- id: end-of-file-fixer
- id: trailing-whitespace
- id: detect-private-key
- id: check-added-large-files
args: ['--maxkb=1000']
args: ["--maxkb=1000"]
- id: check-merge-conflict
- id: check-json
- id: check-toml
Expand All @@ -25,7 +25,7 @@ repos:
hooks:
- id: ty
name: ty check
entry: uvx ty check
entry: uvx ty check . --ignore unresolved-import --ignore unresolved-attribute
language: system
types: [python]
pass_filenames: false
Expand All @@ -34,7 +34,7 @@ repos:
rev: "1.9.4"
hooks:
- id: bandit
args: ['-r', 'hack/', 'experiments/', '--skip', 'B101,B404,B603']
args: ["-r", "hack/", "experiments/", "--skip", "B101,B404,B603"]
pass_filenames: false

- repo: https://github.com/zricethezav/gitleaks
Expand Down
65 changes: 65 additions & 0 deletions experiments/67-claude-github-app-auth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# PoC FullSend #67

This experiment folder addresses: https://github.com/konflux-ci/fullsend/issues/67

1. Create an app with: contents, pull requests, issues. Make it public so it can be installed on any org.
2. Go to the public URL of the app (available on the setting app page after creating it)
3. Install the app on the org of your chosing.
4. Generate a private PEM key for the app (in its settings page).
5. Generate a JWT using the PEM and the App Client ID.
6. Use the JWT to list installations, get the installation id you want.
7. Generate a short lived access token scoped to the repository that you want using the JWT and the installation ID.
8. Export the token and execute claude.

## Results

Passing GH_TOKEN to the environment when executing the agent be enough.

## Example Output

```
Found 2 installation(s):

Installation ID: 119153102
Account: rh-hemartin-konflux (Organization)
Target type: Organization
Token: ghs_4FRsI532... (expires 2026-03-27T11:14:29Z)
Repositories (2):
- rh-hemartin-konflux/konflux-test-app (id: 945843843)
Scoped token: ghs_S9ANYTn2... (expires 2026-03-27T11:14:29Z)
Launching Claude agent for rh-hemartin-konflux/konflux-test-app...
Agent output:
Done! Here's a summary:

- **Issue created:** https://github.com/rh-hemartin-konflux/konflux-test-app/issues/4
- **PR opened:** https://github.com/rh-hemartin-konflux/konflux-test-app/pull/5

The PR adds a description line to the README and references the issue with `Closes #4`, so merging the PR will automatically close the issue.
- rh-hemartin-konflux/testrepo (id: 945993967)
Scoped token: ghs_655syxbA... (expires 2026-03-27T11:15:18Z)
Launching Claude agent for rh-hemartin-konflux/testrepo...
Agent output:
All done. Here's what was created:

- **Issue:** https://github.com/rh-hemartin-konflux/testrepo/issues/20
- **PR:** https://github.com/rh-hemartin-konflux/testrepo/pull/21 (closes issue #20)

The PR adds a small "Claude Agent Test" section to the README on branch `claude-agent-test-20`.

Installation ID: 119149070
Account: rh-hemartin (User)
Target type: User
Token: ghs_h0W4j49l... (expires 2026-03-27T11:16:11Z)
Repositories (1):
- rh-hemartin/nonflux-integration-service (id: 1191345201)
Scoped token: ghs_KlpL3FSB... (expires 2026-03-27T11:16:11Z)
Launching Claude agent for rh-hemartin/nonflux-integration-service...
Agent output:
All done. Here's a summary:

1. **Issue created:** https://github.com/rh-hemartin/nonflux-integration-service/issues/7 — "Testing Claude Agent"
2. **Branch pushed:** `testing-claude-agent-2` with a dummy change adding a "Contributing" section to the README
3. **PR opened:** https://github.com/nonflux/integration-service/pull/6 — "Add contributing section to README" (references `Closes #7`)

Note: The PR was opened against the upstream `nonflux/integration-service` repo since your repo is a fork. The `Closes #7` reference points to the issue in your fork. If you'd prefer the PR to target your fork instead, let me know.
```
129 changes: 129 additions & 0 deletions experiments/67-claude-github-app-auth/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#!/usr/bin/env python3
import os
import subprocess
import sys
import time

import jwt
import requests

pem_path = "..."
client_id = "..."

prompt = """
Use gh to list the available repositories. Create an issue in them called 'Testing Claude Agent',
then clone the repo, introduce a dummy change in the README, create a branch, push the change and
open a PR that solves the issue.
"""

with open(pem_path, "rb") as pem_file:
signing_key = pem_file.read()

payload = {
"iat": int(time.time()),
"exp": int(time.time()) + 600, # 10 minute expiration
"iss": client_id,
}

encoded_jwt = jwt.encode(payload, signing_key, algorithm="RS256")

response = requests.get(
"https://api.github.com/app/installations",
headers={
"Authorization": f"Bearer {encoded_jwt}",
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
},
timeout=60,
)

if response.status_code != 200:
print(f"Error fetching installations: {response.status_code} {response.text}", file=sys.stderr)
sys.exit(1)

installations = response.json()
print(f"Found {len(installations)} installation(s):\n")

gh_headers = {
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
}

for inst in installations:
installation_id = inst["id"]
account = inst.get("account", {})
print(f"Installation ID: {installation_id}")
print(f" Account: {account.get('login', 'N/A')} ({account.get('type', 'N/A')})")
print(f" Target type: {inst.get('target_type', 'N/A')}")

# Create an installation access token
token_resp = requests.post(
f"https://api.github.com/app/installations/{installation_id}/access_tokens",
headers={**gh_headers, "Authorization": f"Bearer {encoded_jwt}"},
timeout=60,
)
if token_resp.status_code != 201:
print(f" ERROR creating token: {token_resp.status_code} {token_resp.text}")
print()
continue

token_data = token_resp.json()
install_token = token_data["token"]
print(f" Token: {install_token[:12]}... (expires {token_data.get('expires_at', 'N/A')})")

# List repositories accessible to this installation token
repos_resp = requests.get(
"https://api.github.com/installation/repositories",
headers={**gh_headers, "Authorization": f"Bearer {install_token}"},
timeout=60,
)
if repos_resp.status_code != 200:
print(f" ERROR listing repos: {repos_resp.status_code} {repos_resp.text}")
print()
continue

repos = repos_resp.json().get("repositories", [])
print(f" Repositories ({len(repos)}):")
for repo in repos:
# Create a repo-scoped token (limited to just this repository)
repo_token_resp = requests.post(
f"https://api.github.com/app/installations/{installation_id}/access_tokens",
headers={**gh_headers, "Authorization": f"Bearer {encoded_jwt}"},
json={"repository_ids": [repo["id"]]},
timeout=60,
)
if repo_token_resp.status_code == 201:
repo_token_data = repo_token_resp.json()
repo_token = repo_token_data["token"]
expires_at = repo_token_data.get("expires_at", "N/A")
print(f" - {repo['full_name']} (id: {repo['id']})")
print(f" Scoped token: {repo_token[:12]}... (expires {expires_at})")

# Launch a Claude agent with the repo-scoped GH_TOKEN
print(f" Launching Claude agent for {repo['full_name']}...")
agent_env = {**os.environ, "GH_TOKEN": repo_token}
result = subprocess.run(
[
"claude", # nosec
"--print",
"--dangerously-skip-permissions",
prompt,
],
env=agent_env,
capture_output=True,
text=True,
timeout=120,
)
print(" Agent output:")
for line in result.stdout.strip().splitlines():
print(f" {line}")
if result.returncode != 0 and result.stderr:
print(" Agent stderr:")
for line in result.stderr.strip().splitlines():
print(f" {line}")
else:
status_code = repo_token_resp.status_code
text = repo_token_resp.text
print(f" - {repo['full_name']} (id: {repo['id']})")
print(f" ERROR creating scoped token: {status_code} {text}")
print()
2 changes: 2 additions & 0 deletions experiments/67-claude-github-app-auth/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
requests==2.33.0
pyjwt==2.12.1