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
31 changes: 31 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: CI

on:
pull_request:
push:
branches: [main]

jobs:
lint-static-test:
name: Lint, static analysis, and tests
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.12"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
- name: Install package
run: python -m pip install -e '.[dev]'
- name: Ruff format check
run: ruff format --check .
- name: Ruff lint
run: ruff check .
- name: Mypy
run: mypy
- name: Pytest
run: pytest
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.coverage
.mypy_cache/
.pytest_cache/
.ruff_cache/
__pycache__/
*.egg-info/
dist/
build/
.venv/
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2026 PickNik Robotics
Copyright (c) 2026 Hermes GitHub App Plugin Contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
104 changes: 104 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Hermes GitHub App Plugin

Hermes plugin for using **per-agent GitHub App identities** instead of a human `gh`/SSH identity.

Each Hermes agent runs the same package but is configured with its own GitHub App:

```yaml
github_app:
client_id: "Iv1.exampleclientid"
installation_id: "987654"
private_key_path: "~/.hermes/secrets/agent-github-app.private-key.pem"
app_slug: "hermes-agent"
```

Environment variables with the same meaning are also supported:

- `GITHUB_APP_CLIENT_ID`
- `GITHUB_APP_INSTALLATION_ID`
- `GITHUB_APP_PRIVATE_KEY_PATH`
- `GITHUB_APP_PRIVATE_KEY` (PEM contents; useful for CI)

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.

## Client ID vs. installation ID

`client_id` identifies the GitHub App registration. GitHub recommends using the GitHub App **client ID** as the JWT `iss` claim when authenticating as an app.

`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`.

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.

## Install

```bash
pip install hermes-github-app-plugin
hermes plugins enable github-app
hermes-github-app setup
hermes-github-app doctor --repo OWNER/REPO
```

`setup` walks through the required values one by one. Optional prompts are explicitly marked with `(optional)`:

```text
GitHub App client ID:
GitHub App installation ID:
GitHub App private key path:
GitHub App slug (optional):
```

For scripted installs, pass flags and skip the network verification until secrets are mounted:

```bash
hermes-github-app setup --non-interactive --skip-verify \
--client-id Iv1.exampleclientid \
--installation-id 987654 \
--private-key-path ~/.hermes/secrets/agent-github-app.private-key.pem \
--app-slug hermes-agent
```

`doctor` checks local installation state and, unless `--skip-network` is set, verifies that an installation token can be minted and the optional repository probe is reachable.

## CLI and wrappers

```bash
hermes-github-app setup
hermes-github-app doctor --repo OWNER/REPO
hermes-github-app status
hermes-github-app token --repo OWNER/REPO
hermes-github-app api --repo OWNER/REPO /repos/OWNER/REPO

gh-app --repo OWNER/REPO pr list -R OWNER/REPO
git-app --repo OWNER/REPO push origin my-branch
```

`gh-app` injects an ephemeral installation token as `GH_TOKEN` and `GITHUB_TOKEN` for the child `gh` process.
`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.

## Migrating existing Hermes skills and jobs

To keep agents from falling back to local human credentials, update existing GitHub-related Hermes skills, cron jobs, and subagent prompts with these rules:

- Use `github_app_*` tools for GitHub API operations when possible.
- Replace authenticated `gh ...` examples with `gh-app --repo OWNER/REPO -- ...`.
- Replace `git push` examples with `git-app --repo OWNER/REPO -- push ...`, or another HTTPS credential-helper flow backed by a freshly minted installation token.
- Do not use `gh auth status` as proof of write identity; it reports local `gh` credentials and may show a human account.
- Avoid SSH remotes for bot-managed worktrees. SSH uses local SSH keys, not the GitHub App token.
- Add a pre-write check with `github_app_verify_identity` or `hermes-github-app status --repo OWNER/REPO`.
- Avoid `@me` assumptions because the GitHub App bot is not the human operator.
- Require write summaries to include the returned `auth_mode`, `app_slug`, `installation_id`, repository, operation, and URL/path.

## Hermes tools

The plugin registers these tools:

- `github_app_status`
- `github_app_verify_identity`
- `github_app_api`
- `github_app_graphql`
- `github_app_create_issue`
- `github_app_comment_issue`
- `github_app_create_pr`
- `github_app_comment_pr`

All mutating tools return auth metadata showing App mode, installation ID, app slug, and target repository.
27 changes: 27 additions & 0 deletions plugin.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: github-app
version: 0.1.0
description: "GitHub App identity integration for Hermes: per-agent app token minting, GitHub API tools, gh/git wrappers, CLI, and workflow skill."
author: Hermes GitHub App Plugin Contributors
provides_tools:
- github_app_status
- github_app_verify_identity
- github_app_api
- github_app_graphql
- github_app_create_issue
- github_app_comment_issue
- github_app_create_pr
- github_app_comment_pr
provides_cli:
- hermes-github-app
- gh-app
- git-app
requires_env:
- name: GITHUB_APP_CLIENT_ID
description: "GitHub App client ID for this Hermes agent. Can also be set in ~/.hermes/config.yaml under github_app.client_id."
secret: false
- name: GITHUB_APP_INSTALLATION_ID
description: "GitHub App installation ID for this Hermes agent. Can also be set in ~/.hermes/config.yaml under github_app.installation_id."
secret: false
- name: GITHUB_APP_PRIVATE_KEY_PATH
description: "Path to the GitHub App private key PEM. Can also be set in ~/.hermes/config.yaml under github_app.private_key_path."
secret: true
60 changes: 60 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
[build-system]
requires = ["hatchling>=1.25"]
build-backend = "hatchling.build"

[project]
name = "hermes-github-app-plugin"
version = "0.1.0"
description = "Hermes plugin for per-agent GitHub App identity, gh/git wrappers, and GitHub App-aware tools."
readme = "README.md"
requires-python = ">=3.10"
license = { text = "MIT" }
authors = [{ name = "Hermes GitHub App Plugin Contributors" }]
dependencies = [
"httpx>=0.25",
"PyJWT[crypto]>=2.8",
"PyYAML>=6.0",
]

[project.optional-dependencies]
dev = [
"mypy>=1.8",
"pytest>=8.0",
"pytest-cov>=5.0",
"ruff>=0.8",
"types-PyYAML>=6.0",
]

[project.entry-points."hermes_agent.plugins"]
github-app = "hermes_github_app_plugin:register"

[project.scripts]
gh-app = "hermes_github_app_plugin.cli:gh_app_main"
git-app = "hermes_github_app_plugin.cli:git_app_main"
hermes-github-app = "hermes_github_app_plugin.cli:main"

[tool.hatch.build.targets.wheel]
packages = ["src/hermes_github_app_plugin"]

[tool.hatch.build.targets.wheel.force-include]
"plugin.yaml" = "hermes_github_app_plugin/plugin.yaml"

[tool.ruff]
line-length = 100
target-version = "py310"

[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP", "SIM", "PL", "RUF"]
ignore = ["PLR0913"]

[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = ["PLR2004"]

[tool.mypy]
python_version = "3.10"
strict = true
packages = ["hermes_github_app_plugin"]

[tool.pytest.ini_options]
addopts = "--cov=hermes_github_app_plugin --cov-report=term-missing"
testpaths = ["tests"]
52 changes: 52 additions & 0 deletions skills/github-app-workflow/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
name: github-app-workflow
description: Use per-agent GitHub App identity for Hermes GitHub operations.
version: 0.1.0
author: Hermes GitHub App Plugin Contributors
---

# GitHub App Workflow

Use this skill for GitHub operations from Hermes agents that are configured with a per-agent GitHub App.

## First-time setup

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, and private key path.

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.

## Rules

1. Prefer `github_app_*` plugin tools for GitHub API operations.
2. Prefer `gh-app` over bare `gh` from the terminal.
3. Prefer `git-app` or HTTPS/App-token credentials over SSH for GitHub writes.
4. Do not rely on `gh auth status` as proof of the desired identity; it often reports the human account.
5. Verify App mode before writes with `github_app_verify_identity` or `hermes-github-app status --repo OWNER/REPO`.
6. Do not use `@me` assumptions; the actor is the app bot, not a human user.
7. Expect comments, PRs, and API writes to appear as `<app-slug>[bot]`.
8. Treat the GitHub App installation scope as the repository access boundary. The plugin does not maintain a separate local repository/owner allowlist.

## Updating existing Hermes skills

Patch any GitHub-related skill, cron prompt, or subagent instruction that mentions `gh`, `git push`, GitHub SSH remotes, or `@me` assumptions:

- Replace bare `gh ...` examples with `gh-app --repo OWNER/REPO -- ...` when the command needs GitHub authentication.
- Replace `git push` examples with `git-app --repo OWNER/REPO -- push ...`, or document an equivalent HTTPS credential-helper flow that uses a freshly minted installation token.
- Add a pre-write verification step: `github_app_verify_identity` or `hermes-github-app status --repo OWNER/REPO`.
- Do not treat `gh auth status` as proof of the write identity. It reports local `gh` credentials and may be a human account.
- Remove or flag SSH remote examples for bot-managed worktrees. SSH uses local SSH keys, not the GitHub App installation token.
- Replace `@me` queries with explicit usernames, teams, or repository-scoped queries because the app bot is not the human operator.
- Require final write summaries to include `auth_mode`, `app_slug`, `installation_id`, `repository`, operation, and URL/path.

For subagents, include the same rules in the delegated prompt because subagents run in separate sessions and may not inherit the parent agent's assumptions.

## Verification

Before reporting success for a write, include:

- repository
- operation
- URL or API path
- `auth_mode: github_app`
- app slug if known
- installation ID
60 changes: 60 additions & 0 deletions src/hermes_github_app_plugin/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""Hermes GitHub App plugin."""

from __future__ import annotations

from pathlib import Path

from . import schemas, tools
from .cli import main as cli_main
from .cli import register_cli

_TOOLSET = "github_app"
_TOOLS = (
("github_app_status", schemas.GITHUB_APP_STATUS, tools.github_app_status, "🤖"),
(
"github_app_verify_identity",
schemas.GITHUB_APP_VERIFY_IDENTITY,
tools.github_app_verify_identity,
"✅",
),
("github_app_api", schemas.GITHUB_APP_API, tools.github_app_api, "🐙"),
("github_app_graphql", schemas.GITHUB_APP_GRAPHQL, tools.github_app_graphql, "📊"),
(
"github_app_create_issue",
schemas.GITHUB_APP_CREATE_ISSUE,
tools.github_app_create_issue,
"📝",
),
(
"github_app_comment_issue",
schemas.GITHUB_APP_COMMENT_ISSUE,
tools.github_app_comment_issue,
"💬",
),
("github_app_create_pr", schemas.GITHUB_APP_CREATE_PR, tools.github_app_create_pr, "🔀"),
("github_app_comment_pr", schemas.GITHUB_APP_COMMENT_PR, tools.github_app_comment_pr, "💬"),
)


def register(ctx: object) -> None:
"""Register Hermes tools, CLI, and bundled skill."""
for name, schema, handler, emoji in _TOOLS:
ctx.register_tool( # type: ignore[attr-defined]
name=name,
toolset=_TOOLSET,
schema=schema,
handler=handler,
emoji=emoji,
)

ctx.register_cli_command( # type: ignore[attr-defined]
name="hermes-github-app",
help="Manage the Hermes GitHub App integration",
setup_fn=register_cli,
handler_fn=cli_main,
description="Mint and verify per-agent GitHub App installation tokens.",
)

skill_path = Path(__file__).parent / "skills" / "github-app-workflow"
if skill_path.exists():
ctx.register_skill("github-app-workflow", skill_path) # type: ignore[attr-defined]
Loading
Loading