Skip to content
Like what we’re building? Star on GitHub

Flow — connect a forge, enable a bot on a repository ​

Connecting a forge seals one credential for a team, and enabling bots on a repository derives a managed token from that credential, then writes the webhook config that enforces the binding, the forge-side hook and the integration row that reports it, in that order.

Drawn from the code at commit f564846d3; how this page is kept true: README.

Three connect modes write the same forge.Connection row: a pasted personal access token validated by WhoAmI before any store write, an OAuth app authorization, or a GitHub App install. The two browser round-trips return on public callbacks (isPublicPath), authenticated by a one-time signed state and the iterion_forge_agent binding cookie compared with subtle.ConstantTimeCompare — the team is resolved from the state, never from the URL. The token is sealed under AAD forge_conn:<id> and the connection is born active.

Connect — the GitHub App path ​

The App path is drawn because it is the only one that calls out twice before persisting: InstallationInfo captures what the owner approved, and only then does MintInstallationToken ask for the intersection of that grant with iterion's wanted set. Asking for a permission the installation lacks fails the whole mint, so the order is load-bearing — the first token already carries the delivery grants.

The sequence is drawn on the feature page: forge-integrations.md § What it does.

Enable — one request, a fixed write order ​

Provision is the one-action enable. It takes a per-repo lease, reserves a durable immutable webhook identity, writes the versioned config, attempts the remote hook, then activates the integration through CAS. A failed or uncertain attempt retains its retry identity; only a confirmed refusal can restore settings through their exact version. Legacy secret migration uses a distinct candidate URL while the old epoch remains active. Same bots, events and URL short-circuit only when no attempt is pending. See ADR-138 for the concurrency guarantee and reconciler boundary.

The write order is drawn on the feature page: forge-integrations.md § What it does.

The admin token the connection holds is never what a bot posts with: ensureManagedSecret re-seals it once per connection as a team-scoped generic_secrets row whose AllowedHosts pin the connection's forge host, and each bot gets that secret id under its declared secret name — as a webhook SecretOverrides entry (Tier-0 for the direct webhook launch) and as a bot_secret_bindings row (so a board-coordinator launch authenticates too). The inbound URL is the connection's WebhookBaseURL pin when set, else the deployment's public URL.

The connection's status ​

StateWritten byLeaves by
activeevery connect path, and a successful worker mint (runtime or security-read)a refusal below, or deletion
needs_reauthmarkRevoked on forge.ErrUnauthorized — it writes this, not revoked — and it withdraws the security-read org entrya later successful mint or deletion — or degraded, when a later mint meets ErrPermissionsNotGranted: the runtime lane's markRevoked leaves AccessTokenExpiresAt in the past and ExpiringBefore filters on kind and expiry only, so the sweep re-mints the connection on every tick
degradedmarkDegraded on the terminal 422 forge.ErrPermissionsNotGranted, stamping the forge message and the remediation on StatusReasondeletion only: refreshOne early-exits on a degraded connection, and every connect path creates a new connection row rather than updating this one
revokedone site only: an avatar apply meeting ErrUnauthorized on a PATdeletion only: a PAT is not refreshable, and further avatar applies refuse

Purpose is its own axis, stamped once at the install callback from the App record (purposeForApp) and never recomputed: a narrowed runtime App must read as broken rather than relabel itself watch-only. The approval record carries no status field — its existence is the pending state, and approve or reject deletes it, leaving the audit trail as the only durable record.

Steps ​

ActorActionCode
team adminstart the self-service App creationhandleStartGitHubManifest, BuildAppManifest
GitHubreturn the temporary code on the public callbackhandleGitHubManifestCallback, ConvertManifest
serverseal the client secret and the private key per tenantcreateForgeOAuthApp, MongoOAuthAppStore.EnsureSchema
team adminpaste a token insteadconnectForgePAT, forgeAdminForToken, admin.WhoAmI, forge.SealPAT
team adminauthorize an OAuth app insteadconnectForgeOAuth, oidc.GenerateStateAndPKCE, handleForgeOAuthCallback
serverresolve which App to install and hand out the install URLconnectForgeGitHubApp, githubAppForInstall, forgeStateStore.put
GitHubreturn installation_id on the public Setup URLhandleForgeGitHubAppCallback, forgeStateStore.take
serverprove the completing user owns the installationforgegithub.VerifyInstallationOwnership, AppConfig.UserAuthConfigured
forgereport the approved permissions, then mintInstallationInfo, RuntimePermissionsFor, SecurityReadInstallationPermissions, MintInstallationToken
storepersist the connection and audit itconnection.SealForgeOAuthTokens, connection.ForgeStore.Create, auditTenant
studiopreview exactly what the enable will subscribe tohandlePreviewForgeEnable, forge.PreviewEnable, forge.ToNativeEvents
team adminrequest the enablehandleEnableForgeRepoBots, canManageTeam, forgeConnForTenant
serverapply the org ex-ante gateprovisionOrgRequiringApproval, Org.RequireProvisionApproval, teamFundsItsOwnRuns
storepark the request verbatim, nothing forge-sideparkProvisionRequest, ProvisionApprovalStore.Create
org adminapprove, replaying it, or rejecthandleApproveProvision, equalStringSets, handleRejectProvision
orchestratorload the connection, find or create the integration, read the previous configOrchestrator.Provision, GetByConnRepo, webhooks.ConfigStore.Get
orchestratorresolve each bot's forge block and invocations, union the eventsBots, Invocations, unionAllEvents, ToNativeEvents
orchestratorderive the managed forge token, once per connectionensureManagedSecret, forgeTokenEgressHosts, secrets.SealGenericSecret
orchestratorbind it per bot, both waysSecretOverrides, ensureBotBinding, bundle.DefaultForgeSecretName
orchestratorbuild the routing surfaces and mint the hook secretbuildCommandMap, resolveBotRules, webhooks.MintToken
storewrite the enforcement halfwebhooks.ConfigStore.Create or Update, carryOperatorWebhookSettings
forgeregister or update the repo hookAdminFor, upsertHook, UpdateHook, GetHook, CreateHook
storewrite the report half, carrying forward what provisioning does not ownRepoIntegrationStore.Create or Update, MinAuthorRole
orchestratornarrow the App token and materialise the schedulesnarrowGitHubAppSecret, syncSchedules
serverre-mint at the point of use, and rotate in the backgroundEnsureManagedSecret, RefreshWorker.refreshOne

Guards that refuse ​

  • Not a team admin — canManageTeam on every connect, oauth-app and repo-bots mutation: 403 admin or owner required. canManageOrg on approve and reject.
  • State store write fails at connect start — 502, rather than handing out an authorize URL whose callback can never complete.
  • Replayed or expired state, mismatched cookie — 400 state expired or invalid (take deletes first, then rejects past the 10-minute TTL, so a replay and an expiry are indistinguishable) and 400 agent binding mismatch.
  • A pasted token the forge rejects — 400 naming the missing scope on forge.ErrUnauthorized; any other WhoAmI failure is 502, because nothing of iterion's own has been written yet.
  • A created App whose present permissions contradict the watch-only profile — 502 and not recorded, naming the page where the orphan App must be deleted; a failure to record one answers 409 with the same link, because its private key is issued once.
  • A shared platform App that cannot verify installation ownership — 412 fail-closed naming ITERION_FORGE_GITHUB_APP_CLIENT_ID and _CLIENT_SECRET; a verified-but-foreign installation is 403 installation is not owned by the authorizing user.
  • An unreadable App record at the install callback — 502 retry the install: a connection born mislabelled runtime from a watch-only App would neutralise every Purpose guard.
  • A watch-only connection — 422 at the enable route, which is not the exhaustive guard: handleUpdateForgeRepoBots and handleApproveProvision reach Provision without one. ensureManagedSecret refuses again on the full-provision path, where it runs before the bindings — which matters because a binding is keyed (tenant, bot, secret_name) and therefore team-global, and nothing rolls it back; the idempotent short-circuit reconciles bindings without passing through it.
  • A cross-tenant connection or integration id — 404 not found, never 403: forgeConnForTenant and ErrConnectionNotFound inside Provision do not leak existence.
  • The org approval gate cannot read the team or the org — 503 provision-approval gate unavailable, never a silent pass; a second pending request for the same tenant, connection and repo is a 409 duplicate.
  • A stale parked request — 409 when the integration vanished, its bot set drifted, the repo was provisioned meanwhile, or the connection is gone; the record stays pending, including after a transient provision failure at replay.
  • A bot with neither a forge: block nor a forge or command invocation — cannot auto-provision; an empty native event set refuses too; two bots claiming the same command without args disambiguation refuse in addCommandRoute.
  • Launch vars that would drop keys the repo pins — ErrLaunchVarsDrop as 400 naming them; an absent map over a diverged integration and config pair is ErrProvisionDiverged as 500.
  • A hook the connection's token may not manage — 403 insufficient_scope on forge.ErrForbidden, 400 connection_unauthorized on forge.ErrUnauthorized, and the durable candidate remains available for retry.

Collections touched ​

CollectionWritten here
forge_oauth_appsthe created or registered App: client secret and private key sealed, unique per tenant, provider, base URL, owner login and watch-only flag
forge_connectionsone row per connect, token sealed under forge_conn:<id>; later stamped with managed_secret_id, and rewritten by the refresh worker
forge_provision_approvalsthe parked request, verbatim, deleted on approve or reject
generic_secretsthe managed forge token, host-pinned, re-minted narrower after the integration write
bot_secret_bindingsone row per enabled bot so a board launch resolves the same token
webhook_configsthe enforcement half: project and event allowlists, bot rules, command map, secret overrides, operator settings, rate limit, hook secret hash
repo_integrationsthe report half: bots, normalized events, webhook and hook ids, operator settings
scheduled_botsreplaced for the integration, one row per schedule invocation
audit_eventsforge.oauth_app.created, forge.connection.created, forge.connection.avatar_applied on the PAT connect's bot-avatar tail, forge.provision.approval_requested on both trails, forge.provision.approved or rejected, forge.integration.provisioned