Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

README.md

github

A self-contained subagent container: a single allowlisted gh (GitHub CLI) command in, gh's own output out -- authenticated as the calling user's own delegated GitHub token, not a shared bot credential (ADR 0022/0027).

Contract

  • Input (argv[2]): everything after gh, e.g. "issue view 86 --repo imaustink/agent-controller --json title,body".
  • Output: gh's own stdout, wrapped in a fenced code block (json when --json was requested, text otherwise), delivered via the event contract in docs/messaging.md.

Identity: acts as the calling user, not a shared bot

Unlike a tool with a static GITHUB_TOKEN baked into its Secret, this tool is designed to run with Tool.spec.identityProviders: [github] set (see charts/community-components/templates/tool-github.yaml). When a Skill routes a call to this tool, agent-orchestrator's runTool (graph.ts) checks the calling user's own linked GitHub identity via apps/integration-gateway's identity-link API (the same OAuth Device Flow broker opencode-swe-agent uses, ADR 0022) and injects the resulting token as a per-invocation GITHUB_TOKEN through ToolRunSpec.secretEnv (ADR 0032) -- never embedding it in the ToolRun CR itself, and never sharing one credential across every caller. A caller who hasn't linked their GitHub account yet gets a clear error asking them to link it via a direct conversation with an identity-linking-capable agent first (v1 scope cut: this tool's call path cannot itself start a fresh device-flow link -- only the peer-level agent-delegation path can, see graph.ts's runTool comment).

A GITHUB_TOKEN/GH_TOKEN env var is all gh needs to authenticate -- src/github.ts sets both directly on the child process so gh never has to run gh auth login or write anything to a persisted config (its GH_CONFIG_DIR is pointed at the container's writable /tmp, wiped every run).

Shared-credential fallback: PAT or GitHub App

A deployment that doesn't want per-user delegation (identityLink.enabled: false, the chart default) has two options, and the chart wires exactly one of them:

  • a static GITHUB_TOKEN fine-grained PAT (githubTool.secretKey), or
  • GitHub App installation-token auth (ADR 0018): set all three of githubTool.githubAppIdSecretKey / githubAppPrivateKeySecretKey / githubAppInstallationIdSecretKey and resolveToolToken (src/github.ts) mints a short-lived, installation-scoped token per invocation instead -- no long-lived PAT has to exist anywhere in the stack. A partial set (1-2 of the 3) is rejected at render time by the chart and at runtime by the tool, rather than silently falling back to the PAT.

resolveToolToken is a thin wrapper over the shared resolveGithubToken (packages/github-app-auth) -- the same precedence (App over static PAT) and the same partial-config rejection every other consumer gets, with the errors re-thrown as GhExecError so an auth failure keeps reporting under this tool's gh_error exit code.

GitHub Enterprise Server: the tool talks to github.com / api.github.com by default. Point it at a GHES install with githubTool.githubHost (wired as GH_HOST, the host gh itself uses) and githubTool.githubApiUrl (wired as GITHUB_API_URL, the REST base App auth mints the installation token against); both are plain env, off by default, and independent of which credential is in effect.

Note that a shared credential and a per-user one are never both configured: the chart wires the App keys only when identityLink is off, and per-user injection only happens when it's on. That exclusivity is enforced in the chart, in one place. If App-as-fallback-for-an-unlinked-caller is ever wanted, it needs an explicit signal the way the SWE agents use GITHUB_IDENTITY_DELEGATION (see their isDelegating) -- token precedence alone can't distinguish a per-user token from a static PAT, since both arrive as GITHUB_TOKEN.

Safety model (defense in depth)

  1. The calling user's own GitHub permissions are the primary boundary. Because this tool authenticates as a specific, identity-linked human (not a broadly-scoped shared bot/App-installation token), the blast radius of anything it does is already bounded by what that person is actually allowed to do on GitHub -- the same posture as opencode-swe-agent (see docs/security.md's "opencode-swe-agent: a deliberately privileged agent" section, ADR 0022).
  2. In-process command allowlist (src/allowlist.ts) -- an explicit allowlist of top-level gh commands + subcommands (issue, pr, repo view/list/clone, release view/list, gist, label, search, workflow view/list, run view/list/watch/download). auth, api, config, secret, variable, ssh-key, gpg-key, codespace, extension, alias, completion, and browse are excluded entirely (see the file header for why each is out of scope for this tool), and a handful of individually irreversible-ish subcommands (repo delete, issue delete/transfer, release delete, workflow run, run cancel/rerun, label delete) are excluded even within an otherwise allowed command. Unlike tools/kubectl-readonly's allowlist, flags/ values are not additionally restricted here -- see the rationale in src/allowlist.ts's header comment (this tool's ServiceAccount/RBAC grants nothing broader than a fixed set of GitHub permissions; GitHub's own authorization on the delegated token is what actually gates a write).
  3. No shell -- the validated argv is passed straight to child_process.spawn, never interpolated into a shell string (src/github.ts).
  4. No persisted credentials -- GH_CONFIG_DIR is pointed at /tmp (wiped every run, root filesystem is otherwise read-only); the token only ever lives in this process's env, sourced from ToolRunSpec.secretEnv (a per-run k8s Secret, garbage-collected with the ToolRun), never written to disk.
  5. Redaction (src/security/redact.ts) -- GitHub's own token prefixes (ghp_/gho_/ghu_/ghs_/ghr_) plus generic Bearer/token patterns are stripped from anything that could reach a progress/ failed event message, in case gh's own error text ever echoes back part of what it was given.

Local development

npm install
npm run typecheck --workspace=github
npm run test --workspace=github
npm run build --workspace=github
docker build -f tools/github/Dockerfile -t github:latest .
GITHUB_TOKEN=ghp_your_pat ./tools/github/run.sh "issue view 86 --repo imaustink/agent-controller"

To test the actual identity-delegation path end-to-end, enable githubTool.enabled=true (and githubTool.identityLink.enabled=true) in charts/community-components, alongside identityLink.enabled=true on both agent-orchestrator and integration-gateway (see charts/agent-controller/values-production.yaml), and invoke it as a real ToolRun/Job in a cluster (e.g. minikube) after linking a GitHub account via chat.