Files
kandev/docs/specs/integrations/system-design/github-authentication-01.md
T

387 lines
26 KiB
Markdown

---
status: draft
system: integrations
requirements:
- REQ-INTEGRATIONS-GITHUB-AUTHENTICATION-001
created: 2026-07-19
owners:
- Kandev
---
# Workspace GitHub Authentication System Design Part 1
## Purpose and boundaries
This design preserves the technical source detail for `REQ-INTEGRATIONS-GITHUB-AUTHENTICATION-001` during migration.
## Requirement mapping
| Requirement | Design section |
| --- | --- |
| `REQ-INTEGRATIONS-GITHUB-AUTHENTICATION-001` | [Migrated source detail](#migrated-source-detail) |
## Migrated source detail
## Why
GitHub credentials must not silently cross workspace boundaries. A local workspace may only need a
human PAT or a named `gh` CLI account, while unattended company automation benefits from a GitHub
App's short-lived, repository-scoped installation tokens. Users also need to keep work and personal
automation under different GitHub Apps without operating separate Kandev deployments.
## What
- Every workspace chooses exactly one automation source: PAT, a named `gh` CLI account, a verified
GitHub App installation, or the migration-only `legacy_shared` source.
- GitHub App registration is configured from the workspace GitHub settings flow. There is no
singleton GitHub App settings page and no automatically active deployment App.
- A workspace may select a GitHub App registration already known to the Kandev deployment, import
an existing GitHub App that the user owns, or create a new GitHub App through GitHub's App
Manifest flow. Import and creation guide the user through ownership, callback, webhook,
permission, visibility, and installation requirements.
- The deployment stores a catalog of GitHub App registrations because a user may intentionally
reuse one App across workspaces. Each workspace still selects and installs an App independently.
Selecting an existing registration never binds another workspace automatically.
- Users who require independent root credentials, bot identity, revocation, or ownership create a
separate registration for each trust boundary. Work and personal workspaces can therefore use
different Apps.
- Reusing one registration shares its App private key, client secret, webhook secret, permission
policy, and bot identity. Installation tokens, workspace repository scope, connection generation,
broker leases, health, and personal OAuth tokens remain workspace isolated.
- A newly created App defaults to private, meaning GitHub permits installation only on the account
that owns it. The user may explicitly choose public when the same App must be installable on other
GitHub accounts or organizations. Public does not list the App in Marketplace, reveal secrets, or
grant repository access without installation approval.
- PAT and named CLI automation act as the verified human account. A separate `My GitHub` connection
is only offered when workspace automation uses a GitHub App, because App installations are not
people and cannot provide authenticated-viewer semantics.
- Named CLI account discovery accepts a successful `gh auth status` report from either stdout or
stderr, while preserving non-empty stdout as the authoritative command result.
- User-triggered mutations prefer the workspace's verified personal connection, then a human
PAT/CLI automation connection, then the App installation. The UI always identifies the effective
actor and never labels an App mutation as human-attributed.
- Background watches, cleanup, repository discovery, and workflow sync always use the workspace
automation connection. Personal credentials are never exposed to agents or executors.
- Task Git credential routing is a separate workspace policy. **Managed workspace credentials**
gives attached GitHub repositories task/repository/generation-bound broker leases from the
workspace automation connection. **Inherit executor Git credentials** injects no GitHub broker
helper or `gh` shim: Local and Worktree tasks use host-visible Git/SSH credentials, while remote
tasks use credentials configured in that executor.
- A task that is explicitly created for a pre-PR fork contribution under managed task credentials
may carry one server-authored, versioned `contribution_destination` on its canonical repository
attachment. Managed routing may issue one additional lease for that exact fork. The fork is a
push destination only: canonical repository identity, `origin`, issue lookup, and pull-request
targeting remain unchanged. Executor-owned identities are opaque and do not receive a
workspace-authored destination binding.
- Every newly created workspace attempts to persist **Inherit executor Git credentials** as its
initial task policy. After a successful settings write, if creation is performed by an internal
trusted caller, an auth-disabled synthetic administrator, or a real administrator while host
`gh` has an authenticated active account, Kandev also stores that exact host/login as the new
workspace's named CLI automation connection. Non-admin member-created workspaces never receive
the server operator's CLI identity automatically.
- If host `gh` is absent, unauthenticated, or cannot validate its active account, workspace creation
still succeeds with executor task access and disconnected GitHub automation. If the executor
settings write fails, creation still succeeds but the existing managed compatibility fallback
remains until the workspace is configured or retried. Existing workspaces are never migrated to
these defaults; their connection, persisted policy, and legacy missing/invalid policy fallback
remain unchanged.
- Workspace settings present the automation identity and task Git credential routing as one
**Workspace GitHub access** group. The page shows a compact read-only summary of both effective
choices; the existing **Change GitHub connection** dialog contains the full controls for the
automation method and task routing policy. The task policy remains independent in behavior and
persistence even though the related choices share one configuration surface.
- The connection dialog has one **Save changes** action for local PAT or named CLI connection
drafts and task Git access. Method-specific **Connect token**, **Use account**, and **Save task
access** actions are not shown. GitHub App create, import, and install remain explicit workflow
actions because they leave Kandev for GitHub rather than saving a local connection draft.
- For Kandev-managed GitHub checkouts used by Local and Worktree tasks, the selected task policy
also controls the persisted `origin` transport. Managed routing uses canonical GitHub HTTPS.
Executor inheritance resolves the current `github.com` protocol from host `gh` configuration at
the clone or origin-reconciliation boundary. Resolution accepts the host-specific value first,
then the global value, and defaults to SSH when neither scope supplies `ssh` or `https`. Launch
and resume do not use a backend-startup snapshot, so a later host configuration change applies
without restart. This makes Git conditional includes based on `remote.*.url` observe the same
transport the task uses. Kandev never rewrites the remote of a repository registered as a
user-managed local checkout.
- Repository preparation resolves each attached repository once per launch or resume and reuses
that result for primary-repository configuration, multi-repository configuration, and credential
routing. The clone-protocol resolver itself is stateless and injected at the repository URL
construction boundary. The long-lived cloner and review-repository adapter do not store a
resolved protocol. Origin reconciliation is serialized per managed checkout. It compares the
current and desired canonical URLs, and performs no write when they already match.
- Every launch or resume entry point prepares repositories before the agent starts. This includes
the prepared-workspace path that reuses a running executor. Repository preparation resolves each
attached repository once for that operation. The full launch path reuses the result for primary
and multi-repository configuration and credential routing. Origin reconciliation is serialized
per managed checkout. It compares the current and desired canonical URLs and performs no write
when they already match.
- Repository preparation validates any contribution destination before issuing credentials, adds a
collision-resistant dedicated fork remote, and reconstructs it on launch and resume. It never
accepts a fork inferred from the checkout's current remotes or a caller-provided repository name.
- Git failures while inspecting or reconciling a managed checkout preserve a bounded,
credential-redacted diagnostic. Git's dubious-ownership failure is classified as a service/data
ownership mismatch with guidance to restore the intended Kandev service account or reconcile the
managed data owner. Kandev does not bypass Git's ownership protection with a broad
`safe.directory` entry.
- Under managed routing, App installation tokens are minted for the requested repository and cached
only in memory. PAT/CLI tokens retain their provider-granted scope once delivered to a trusted
agent subprocess. GitHub HTTPS and the broker-aware `gh` shim fail closed rather than consulting
another ambient helper after a managed-helper failure.
- A managed pre-PR fork destination must be writable by the same workspace automation source that
supplies the task leases. Human PAT and named CLI automation actors may own or create their fork.
An App installation without direct target write access is not silently paired with a user's
personal fork or personal token.
- Managed Git helper execution does not depend on the post-startup `PATH`: Git resolves an
absolute Kandev-owned `agentctl` executable published before the first managed Git operation.
Local and Worktree preparation binds the helper to the standalone launcher's absolute executable
before checkout or setup scripts run. Remote preparation binds it to the installed executor
binary before cloning, and a running `agentctl` publishes its own executable for child processes.
Non-interactive Unix login shells that replace their inherited `PATH` restore the managed
CLI-shim directory after profile initialization for broker-enabled tasks, while preserving
pre-existing Bash environment hooks, including hook paths containing `$VAR` or `${VAR}`
references from the effective child environment. Broker-disabled and executor-inheritance
processes receive no shell hook or managed-tool path.
- Under managed routing, every authorized task execution surface receives the same current
task-scoped Git environment: the agent subprocess, terminal shells, passthrough-agent PTYs, and
task-scoped command processes. This includes the broker contract, managed indexed Git
configuration, and the `agentctl`/`gh` shim-first `PATH`; it does not grant access to a browser
client, an unrelated host shell, or another workspace's task environment.
- Kandev composes the indexed `GIT_CONFIG_COUNT` / `GIT_CONFIG_KEY_<n>` /
`GIT_CONFIG_VALUE_<n>` protocol across host, executor, profile, task, and agentctl boundaries.
Unrelated entries such as hooks, notes, safe-directory, and URL-rewrite settings survive in their
original order; later Kandev credential entries affect only their intended GitHub credential
keys. An already-forwarded suffix is not duplicated.
- Explicit executor-profile `GITHUB_TOKEN` or `GH_TOKEN` values remain unmanaged operator overrides
and take precedence over the workspace broker when managed routing is selected.
- Every successful task launch or resume records a non-secret session snapshot of the selected
policy, effective credential source, known method and actor, transport, executor, and capture
time. Inherited and profile-token actors are labeled runtime-selected instead of being probed.
- The unpublished `KANDEV_GITHUB_APP_*` configuration introduced on this branch is removed. Setting
those variables does not create or configure a registration; operators use the guided import
flow for an App they already own.
- Existing released workspaces migrate to `legacy_shared`; upgrades do not rewrite their
connection or task policy. New operator-authorized workspaces use the active authenticated host
`gh` account when one can be validated, while member-created workspaces and workspaces created
without usable host `gh` remain disconnected. Once a workspace leaves legacy mode it cannot
return. The unpublished singleton registration
schema on this branch is rewritten directly and receives no compatibility migration. A valid
legacy connection supplies both the existing API client abstraction and an in-memory Git
transport credential so provider-backed repositories can be rematerialized from legacy shared
managed paths into workspace-isolated clone roots.
- Copying a workspace copies repository preferences but never copies a PAT, CLI account selection,
App installation binding, registration secret, or personal identity.
## Choosing A Method
| Method | Use when | Benefits | Costs and limits |
| --- | --- | --- | --- |
| PAT | A local or simple workspace should act as one person | Fast setup; human attribution | Long-lived bearer secret; agents receive its full provider scope |
| Named `gh` CLI | The desired human account already exists in local `gh` auth | No token copied into Kandev; deterministic account selection | Depends on host CLI credentials; remote agents still use the brokered bearer token |
| GitHub App | An organization or unattended workspace needs managed automation | Short-lived repository-scoped tokens; independent revocation; App attribution | Requires App ownership, callback/webhook configuration, installation, and a public HTTPS URL for full lifecycle health |
The workspace UI explains that an App is recommended for background jobs and managed agents, but
it does not claim agents can only use the selected method. Explicit executor credentials and other
unmanaged tools remain outside Kandev's workspace credential contract.
The automation method and task Git credential policy are independent. Selecting a named `gh`
account does not inherit the host's Git configuration: in managed mode Kandev resolves that exact
host/login and brokers its token to the task. Users who want traditional host or executor behavior
select **Inherit executor Git credentials** explicitly.
## Identity And Routing
| Purpose | First choice | Fallback | Attribution |
| --- | --- | --- | --- |
| Background reads and writes | Workspace automation | None | Automation principal |
| Managed task git and `gh` access | Workspace automation | Explicit profile token | Automation principal, or runtime-selected override |
| Inherited task Git access | Executor-visible Git/SSH credentials | None | Runtime-selected |
| `My GitHub` reads | Personal connection | Human PAT/CLI automation | Human principal |
| User-triggered mutation | Personal connection | Human PAT/CLI, then App installation | Effective principal shown in UI |
An App-only workspace without personal OAuth remains usable for automation and App-attributed
mutations. `My GitHub` instead offers a personal connection created with the same App registration
as the workspace installation.
## GitHub App Policy
Kandev-created Apps request repository metadata read; contents read/write; pull requests read/write;
issues read/write; checks, statuses, and Actions read; administration read; organization members
read; and workflows write. The UI exposes this policy through a permissions button and dialog, not
a row of chips. The App subscribes to the configurable `push` and `check_run` events. GitHub sends
`installation`, `installation_repositories`, and `github_app_authorization` lifecycle events
automatically, so they are handled but are not part of the requested event policy.
An imported App must meet the same callback, OAuth-on-install, webhook, event, and permission
requirements. Kandev validates the App identity and reports missing capabilities after installation.
It does not silently change an imported App's GitHub settings. The guide provides exact values and
GitHub links for the user to apply.
## Data Model
### `github_app_registrations`
One row per GitHub App known to this Kandev deployment. Registration metadata is catalog state;
workspace use is represented only by an explicit workspace connection.
| Field | Type | Constraint |
| --- | --- | --- |
| `id` | UUID text | Primary key; allocated before manifest creation |
| `source` | enum text | `managed` (manifest-created) or `imported` |
| `display_name` | text | Non-empty user-facing disambiguator |
| `github_host` | text | `github.com` in this feature |
| `app_id` | int64 | Positive; unique with `github_host` |
| `client_id` | text | Non-secret App OAuth client identifier |
| `slug` | text | Verified App slug |
| `owner_login` | text | Verified App owner |
| `owner_type` | enum text | `User` or `Organization` |
| `visibility` | enum text | `private` or `public` |
| `public_base_url` | text | Canonical public HTTPS origin |
| `created_for_workspace_id` | text nullable | Provenance only; not an automatic binding |
| `credential_generation` | int64 | Positive; cache and lease invalidation key |
| `credential_secret_id` | text | Non-empty encrypted bundle pointer |
| `status` | enum text | `active` or `invalid` |
| `webhook_status` | enum text | `unverified`, `verified`, or `failing` |
| `last_webhook_at` | timestamp nullable | Last correctly signed delivery for this registration |
| `last_error` | text nullable | Sanitized validation or runtime failure |
| `created_at`, `updated_at` | timestamp | Required |
Managed and imported credentials are immutable encrypted bundles under
`github:app-registration:<registration-id>:g<generation>:<nonce>`. A versioned bundle contains the
private key, client secret, and webhook secret. Metadata points to the active bundle only after the
bundle is durable. Every registration is represented by one verified catalog row and encrypted
bundle; there is no synthetic, configuration-backed, or globally active registration.
### `github_app_registration_flows`
Manifest flows store `state_hash`, preallocated `registration_id`, initiating `workspace_id` and
`user_id`, owner type/login, display name, visibility, canonical public base URL, manifest revision,
expiry, consumption time, and creation time. A new flow for the same workspace supersedes its older
unconsumed flow. State is random, hashed at rest, single-use, and expires after one hour.
### `github_workspace_connections`
One row per configured workspace automation identity. Existing fields remain, with:
| Field | Type | Constraint |
| --- | --- | --- |
| `app_registration_id` | UUID text nullable | Required only for `github_app_installation`; FK to `github_app_registrations.id` with delete restricted |
For an App connection, `installation_id`, verified account login/type, and
`app_registration_id` must all be present. PAT/CLI/legacy rows must have no registration ID.
### `github_workspace_settings`
The non-secret operational settings row adds `task_git_credentials_mode`, with allowed values:
| Value | Behavior |
| --- | --- |
| `managed` | Inject the workspace broker contract for attached GitHub repositories unless an explicit executor-profile token overrides it. Existing missing/invalid values continue to normalize here for upgrade compatibility. |
| `executor` | Default persisted for newly created workspaces. Inject no broker helper or managed `gh` shim. Local/Worktree HTTPS can use the optional host CLI bridge described in part 2. Remote credentials remain executor-owned. |
Missing or invalid persisted values normalize to `managed`. Workspace-settings copy includes this
policy because it is operational configuration, not authentication material.
### Task-session credential snapshot
After a successful launch or resume, task-session metadata contains a versioned
`git_credential_snapshot` with:
- selected policy (`managed` or `executor`);
- effective source (`workspace`, `executor_profile`, or `executor`);
- workspace method when known (`pat`, `gh_cli`, `github_app_installation`, or
`legacy_shared`);
- a known human/App actor label, or `runtime_selected` when Kandev does not inspect the credential;
- transport (`managed_https`, `profile_token`, or `executor_selected`), executor type, and capture
timestamp.
The snapshot contains no token, lease, helper path, credential-file path, or SSH key detail. A
failed launch/resume does not replace the last successful snapshot.
### `github_user_connections`
One optional personal identity per `(workspace_id, user_id)`. Add required
`app_registration_id`, which must equal the current workspace App connection's registration. Access
and refresh tokens remain encrypted under workspace/user-derived keys. Switching away from that
App deletes the old personal secrets and increments its generation before the new automation
connection becomes visible.
### `github_auth_flows`
Installation and personal OAuth flows include `app_registration_id` in addition to workspace,
user, expected connection generation, PKCE material, expiry, and consumption state. A callback is
valid only when both its route registration ID and stored registration ID match.
### `github_webhook_deliveries`
The primary key is `(app_registration_id, delivery_id)`. A delivery is claimed only after the
registration-specific HMAC signature is verified. The row records event, terminal result, received
time, and processed time without payload secrets or tokens.
Background PR, task, and review-watch records retain `workspace_id`; review watches also retain the
verified target human login. Missing or contradictory ownership fails closed.
## API Surface
All non-public endpoints require workspace authorization. The current trusted-single-user runtime
maps this to `default-user`; mutually untrusted deployments require real workspace/admin roles
before exposing registration management.
### Registration catalog
- `GET /api/v1/github/app/registrations?workspace_id=<id>` returns accessible non-secret
registrations, source, identity, visibility, callback URLs, health, whether each is selected by
this workspace, and sharing implications. It never returns credentials.
- `POST /api/v1/github/app/registrations/manifest/start` accepts `workspace_id`, `display_name`,
owner type/login, visibility, and public base URL. It returns the GitHub owner-specific manifest
submission URL, generated manifest, state, registration ID, revision, and expiry.
- `GET /api/v1/github/app/registrations/:registrationId/manifest/callback` consumes state, converts
the one-hour code, verifies identity and policy, commits an encrypted bundle plus metadata, and
returns to that workspace's GitHub settings. It does not select or install the App automatically.
- `POST /api/v1/github/app/registrations/import/prepare` creates a short-lived, single-use import
preparation for the initiating workspace. It returns `registration_id`, `public_base_url`,
`manifest_callback_url`, `install_callback_url`, `personal_callback_url`, `webhook_url`,
`permissions`, `events`, and `expires_at` so the user can configure the existing App before
submitting any root credentials. The legacy `setup_url` response field is retained for client
compatibility and is always empty because OAuth-on-install disables GitHub's Setup URL.
- `POST /api/v1/github/app/registrations/import` consumes the prepared `registration_id` and accepts
the workspace context, label, App ID, client ID/secret, slug, private key, webhook secret, owner,
and visibility. It verifies the App via GitHub before atomically persisting it. Duplicate
`(host, app_id)` returns `github_app_already_registered` and the non-secret existing registration
ID.
- `PATCH /api/v1/github/app/registrations/:registrationId` changes only `display_name`.
- `DELETE /api/v1/github/app/registrations/:registrationId` deletes a registration only when no
workspace or personal connection references it.
### Workspace automation
- `GET /api/v1/github/status?workspace_id=<id>` returns automation, personal identity, effective
actors, App registration metadata, capabilities, missing permissions, and migration state.
- `GET /api/v1/github/workspace-settings?workspace_id=<id>` returns repository scope, saved
preferences, and `task_git_credentials_mode`.
- `PUT /api/v1/github/workspace-settings` accepts a partial
`task_git_credentials_mode` update and rejects unknown values without changing the prior policy.
- `GET /api/v1/github/auth/gh-cli/accounts?workspace_id=<id>` lists exact local host/login choices.
- `PUT /api/v1/github/workspace-connection?workspace_id=<id>` configures validated PAT or named CLI
auth. App connections can only be committed by the verified installation callback.
- `POST /api/v1/github/app/install/start` accepts `workspace_id` and `app_registration_id`, stores a
single-use flow, and returns the registration-specific GitHub installation URL.
- `GET /api/v1/github/app/registrations/:registrationId/install/callback` verifies state, App,
installation, authorizing user, and owner association before atomically replacing workspace
automation. Failure leaves the previous automation connection unchanged.
- `DELETE /api/v1/github/workspace-connection?workspace_id=<id>` removes workspace secret material
and the App installation binding but never deletes or uninstalls the registration.
- `POST /api/v1/github/app/registrations/:registrationId/webhook` is public. It chooses exactly that
registration's webhook secret, validates HMAC before parsing or claiming the delivery, and only
mutates connections whose registration and installation both match.
### Personal identity
- `POST /api/v1/github/personal-connection/start` uses the workspace's active App registration and
returns its PKCE/state authorization URL.
- `GET /api/v1/github/app/registrations/:registrationId/personal/callback` validates route, state,
PKCE, current workspace registration, and GitHub user before storing tokens.
- `DELETE /api/v1/github/personal-connection?workspace_id=<id>` deletes only the current user's
workspace personal connection and secrets.
Existing `/api/v1/github/token` remains a one-release compatibility alias with mandatory
`workspace_id` and a deprecation header.