ADR-0007: Forge Integration (GitHub + Gitea) and Authentication
Context and Problem Statement
Onboarding to Stet is "sign in with GitHub." The app lists pull requests and files that matter to the user, watches for new/updated PRs, reads diffs and file contents, and — after a dispatched agent does its work — creates pull requests. The user also self-hosts Gitea, which must be a first-class forge, not an afterthought. Local git operations (worktrees, diffs, blame) are also needed.
We must decide the authentication mechanism, credential storage, how forge data is synced, and the local git plumbing.
Decision Drivers
- First-run is "sign in with GitHub." OAuth, not personal access tokens pasted into a field.
- Gitea is first-class. The design must generalize across GitHub and Gitea (both speak OAuth2 and a REST API).
- Native, secure credential storage. Tokens belong in the macOS Keychain.
- Timely PR awareness without heavy infrastructure. The app should learn about PR changes reasonably promptly on a desktop app's terms.
- Local git operations in-process. Worktrees, diffs, and blame should not shell out fragilely.
- Scoped, revocable dispatch credentials. Agents (ADR-0003) get a branch-scoped credential, not the user's full token.
Considered Options
- Auth: OAuth device flow vs. loopback
ASWebAuthenticationSessionvs. PAT paste. - Sync: webhooks vs. polling vs. hybrid.
- Local git: SwiftGit2 (libgit2) vs. shelling out to
git.
Decision Outcome
Chosen: A Forge protocol implemented for GitHub and Gitea, authenticated via
OAuth (loopback ASWebAuthenticationSession where a browser round-trip is
fine; device flow as the headless/fallback path), with tokens stored in the
macOS Keychain. Forge data is synced by polling on an adaptive interval, with
webhook support where the user can provide a reachable endpoint (hybrid,
polling-first so it works with zero server setup). Local git uses
SwiftGit2 (libgit2 bindings) for
in-process worktrees, diffs, and blame. Each dispatched agent receives a
branch-scoped credential that is revoked on sandbox teardown.
Consequences
- Good, because "sign in with GitHub/Gitea" is the intended first-run, with secure native token storage.
- Good, because one
Forgeprotocol keeps GitHub and Gitea genuinely equal. - Good, because polling-first sync needs no server infrastructure; webhooks are an optional upgrade.
- Good, because SwiftGit2 gives robust in-process git without brittle shelling.
- Bad, because polling has latency/rate-limit tradeoffs to tune per forge.
- Bad, because SwiftGit2 wraps a C library (libgit2 build/linking, memory management at the FFI boundary).
- Neutral, because branch-scoped credential issuance depends on forge token capabilities and may differ between GitHub and Gitea.
Confirmation
- Sign-in completes an OAuth flow for GitHub and for Gitea; the token lands in the Keychain and is reused across launches.
- A
Forgeprotocol abstracts PR listing, diff/content reads, and PR creation for both forges. - New/updated PRs appear in the inbox within the adaptive polling window (and promptly via webhook when configured).
- Worktree/diff/blame operations run in-process via SwiftGit2.
- Dispatched agents use a branch-scoped credential revoked on teardown.
Pros and Cons of the Options
Auth: OAuth (loopback + device) vs. PAT paste
- OAuth: Good, real first-run, revocable, scoped. Bad, more moving parts.
- PAT paste: Good, trivial. Bad, wrong UX, insecure habit, no clean scoping.
Sync: polling-first hybrid vs. webhooks-only vs. polling-only
- Hybrid: Good, zero-setup default with an upgrade path. Bad, two code paths.
- Webhooks-only: Good, prompt. Bad, requires reachable endpoint — untenable default for a desktop app.
- Polling-only: Good, simplest. Bad, latency and rate-limit pressure.
Local git: SwiftGit2 vs. shelling to git
- SwiftGit2: Good, in-process, typed, robust. Bad, C dependency at FFI.
- Shell git: Good, no dependency. Bad, brittle parsing, process overhead.
More Information
SwiftGit2 wraps libgit2 (macOS supported). Branch-scoped credentials feed the sandbox in ADR-0003; PR creation is the tail of the loop in ADR-0006. See SPEC-0001.