Skip to content

Add a github-app connection whose tokens gete issues itself - #83

Merged
haruotsu merged 2 commits into
mainfrom
github-app-connection
Sep 30, 2026
Merged

haruotsu merged 2 commits into
mainfrom
github-app-connection

Conversation

@haruotsu

Copy link
Copy Markdown
Collaborator

Summary

Adds github-app, a catalog connection whose tokens gete issues itself from a GitHub App's private key, instead of receiving a user's token from Gemini Enterprise. Agents use it from ordinary openapi blocks (and python tools through ConnectionClient), so operations, params, does_not, and the host check apply exactly as they do today.

# gete.yaml
connections:
  github-app:
    base_url: https://ghe.example.com/api/v3   # leave out for github.com
    app:
      app_id: "123"
      private_key_secret: ge-github-app-private-key
      # The ceiling of every token issued through this connection
      repositories: [example-org/requests]
      permissions: {issues: read}

# agent.yaml
connections: [github-app]
tools:
  - openapi:
      spec: ./specs/github.yaml
      connection: github-app
      effect: read
      operations: [SearchIssues, GetIssue, ListIssueComments]
      params:
        SearchIssues:
          q: {prefix: "repo:example-org/requests is:issue "}

What gete does for such a connection

  • Signs an App JWT (RS256, iat backdated 60s, valid for under 10 minutes) and finds the installation from the first permitted repository the App is installed on (GET /repos/{owner}/{repo}/installation).
  • Issues a narrowed installation token (POST /app/installations/{id}/access_tokens with repositories and permissions) and reuses it in-process until 5 minutes before expires_at. A 401 from the service drops the cached token so the next request gets a fresh one.
  • Creates no Gemini Enterprise authorization. register skips the connection, --reset-authorization does not accept it, and no reauthorization tool is offered. A missing key or a refused issue is reported to the user as text (status code only; response bodies and key contents never appear).
  • Delivers the private key like secret_env. private_key_secret is wired into the Terraform module call as GETE_APP_KEY_<CONNECTION>, which the agent may not set itself; validate --check-secrets checks it. The App ID and the ceiling travel in the resolved declaration.

Schema and validation

  • A connection now declares exactly one of oauth or app.
  • The catalog entry leaves app open (like {base_url}); validate refuses an agent holding it until app_id, private_key_secret, repositories, and permissions are set. permissions is required so a token never carries the installation's whole grant.
  • repositories must share one owner, since a token comes from one installation.
  • Scope selections and mcp blocks on an app connection are refused. MCP is left for a follow-up: ADK reads MCP headers synchronously, while issuing a token is a request of its own.

Other changes

  • gete connections github-app describes the App, key secret, delivery variable, and ceiling instead of OAuth client details.
  • gete run no longer asks for GETE_TOKEN_* for app connections; the issuer reads the PEM from GETE_APP_KEY_*.
  • cryptography is declared as a direct dependency (already installed through ADK) since gete now signs the JWT itself.

Test plan

  • uv run pytest (934 passed)
  • uv run mypy
  • uv run ruff check / uv run ruff format --check

Some agents need to read the same repositories whoever calls them, and a
per-user token forwarded by Gemini Enterprise cannot give them that. A
GitHub App installation token can, but until now the only way to use one
was a python tool holding its own credential, outside the connection's
host check and the openapi block's operations, params, and does_not.

The new connection kind keeps every one of those guards: the token is
issued from the App's private key inside gete's client, narrowed on every
issue to the repositories and permissions gete.yaml declares, and sent
only to the connection's hosts. Nobody approves anything, so register
creates no authorization and the agent offers no reauthorization tool;
a missing key or a refused issue reaches the user as text. The key is
delivered like secret_env, so an agent cannot swap it for its own.

MCP blocks are refused for now: ADK reads MCP headers synchronously,
while issuing a token is a request of its own.

🤖 Generated with Claude Code
…bots

An app connection acts as the App for whoever calls the agent, the same
trust model as a shared credential, but gete graph drew it like a
connection carrying the caller's authorization. It is now marked (bot).

The private key could issue a token with the installation's whole grant,
above the ceiling gete.yaml declares. Left in GETE_APP_KEY_*, any tool
reading its settings, and any process it starts, would find it. The agent
build now takes it out of the environment before the agent's own modules
are imported. This is not a sandbox against code in the same process,
which the README now says.

A bare app_id in YAML is read as a number, and the schema refused it
although the loader already turned it into a string; both forms are
accepted now.

🤖 Generated with Claude Code
@haruotsu

Copy link
Copy Markdown
Collaborator Author

Pushed ddbd9f5 with three follow-ups:

  • gete graph marks app connections as (bot). An app connection acts as the App for whoever calls the agent, like a shared credential, so it no longer reads as if it carried the caller's authorization. Both the edge to its tools and the node of an unused connection are marked.
  • The App's key is taken out of the environment. The key can issue a token with the installation's whole grant, above the declared ceiling. When the agent is built, before its own modules are imported, gete moves GETE_APP_KEY_* out of os.environ and keeps it inside the issuer, so tools reading their settings and processes they start no longer find it. This is not isolation from code in the same process; the README now says so and asks to keep the App's own grant minimal.
  • app_id may be written unquoted. YAML reads a bare ID as a number; the schema now accepts a positive integer as well as a digit string, and the loader already turned it into a string.

uv run pytest (944 passed), uv run mypy, uv run ruff check, uv run ruff format --check all pass.

@haruotsu
haruotsu merged commit 44fdddf into main Sep 30, 2026
3 checks passed
@haruotsu
haruotsu deleted the github-app-connection branch September 30, 2026 10:04
@github-actions github-actions Bot mentioned this pull request Sep 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant