The GitHub integration has three parts: one GitHub App for bot and teammate
credentials, webhook intake on Public ingress,
and the gh CLI used inside trusted runs. Installation and App user tokens are
the only GitHub credentials Open Session accepts.
For a team install, create one organization-owned GitHub App. A single-user simple-mode install may instead use a personal App. The same App provides:
- short-lived installation tokens for reviews, comments, merges, clones, pushes, previews, sandboxes, and trusted GitHub automations;
- device-flow user tokens so interactive sessions act as the signed-in person;
- the bot identity
<app-slug>[bot]for self-trigger protection and attribution.
Configure it from Settings → Integrations, or under
integrations.github in ~/.opensession/config.json:
The private key is not stored in JSON. Upload the PEM in Settings →
Integrations; Open Session writes it atomically with mode 0600 to ~/.opensession/github-app.pem. Operators
may instead set OPENSESSION_GITHUB_APP_KEY to an externally managed PEM path.
The UI will not overwrite or delete an operator-managed key.
Environment overrides for the App identity are
OPENSESSION_GITHUB_CLIENT_ID, OPENSESSION_GITHUB_CLIENT_SECRET,
OPENSESSION_GITHUB_APP_SLUG, and OPENSESSION_GITHUB_APP_KEY (a path, not PEM
contents). Environment values win over config. installationOwner is required
for service work and verifies repository ownership; installationId may also
pin its known numeric installation.
The create-App link in Settings → Integrations is generated from the same canonical permission set used when tokens are minted:
| Scope | Access | Why |
|---|---|---|
| Actions | Read | failing workflow logs for trusted fixes |
| Checks | Read | check runs |
| Commit statuses | Read | status rollups |
| Contents | Read and write | clone and push |
| Deployments | Read | preview deployment state |
| Issues | Read and write | issue and PR comments |
| Metadata | Read | GitHub baseline |
| Pull requests | Read and write | reviews, PRs, merges |
| Members (organization) | Read | roster and attribution |
Enable Device Flow, generate a client secret and private key, then install the App only on the organization and repositories Open Session should reach. When permissions change, approve the updated installation permissions too.
GitHub service authority is fail-closed. A missing key, wrong installation
owner, unapproved permission, or failed token mint never falls back to ambient
gh, a host SSH key, or a connected human. Installation tokens remain process-local and
short-lived; repository code runs receive a token scoped to that one verified
repository.
Set appSlug even though token minting itself only needs the client id and key.
App-authored activity appears as <app-slug>[bot]; Open Session adds that login
to its own-author set so comments and pushes cannot trigger loops. The App slug
itself is the preferred PR mention handle. Keep old names in
integrations.github.mentionHandles only as compatibility aliases.
policy.githubBotLogins may retain aliases for historical App names.
GITHUB_MENTION_HANDLES adds compatibility mention handles. Server-owned gh
calls receive a short-lived App token in their process environment. HTTPS Git
operations use a process-local credential helper, and SSH GitHub remotes are
rewritten to HTTPS for that process so host keys cannot bypass the App.
The fail-closed public ingress gateway listens on 127.0.0.1:3860. Choose
Cloudflare Tunnel or Direct HTTPS with Caddy under Settings → Domains and
ingress → Public callbacks. Never route the private app port through that
public origin.
- Route:
POST /github/webhook(registered by the GitHub agent,packages/core/opensession-server/src/agents/github/index.ts). For an existing Slack-only deployment with GitHub disabled, Slack registers the same GitHub-owned handler as a compatibility fallback. When both are enabled, only GitHub registers it. - Verification:
GITHUB_WEBHOOK_SECRET, HMAC-SHA256 over the raw body, headerx-hub-signature-256(sha256=<hex>), timing-safe compare; invalid signature → 401. The body limit is 1 MiB. Deliveries are deduped byx-github-deliveryfor 24 hours, with at most 500 ids retained.
Use the App-level webhook, not one repository webhook per repo. In the GitHub
App's General → Webhook settings, set the public URL to
https://<public-origin>/github/webhook, make it active, and paste the same
strong secret stored as GITHUB_WEBHOOK_SECRET in Settings → Integrations or
~/.opensession.env (for example, generate one with openssl rand -hex 32).
Then under Permissions & events → Subscribe to events, select Issue
comments, Pull request review comments, Pull request reviews, Pull
requests, and Workflow runs. The generated Create GitHub App link
pre-fills the URL and active state, but it cannot fill the secret or event
subscriptions. Restart Open Session after setting or changing the secret; the
GitHub-side subscription checkboxes take effect without an Open Session restart.
These are the subscribed events the code consumes
(packages/core/opensession-server/src/agents/github/webhook.ts):
| Event | What happens |
|---|---|
issue_comment, pull_request_review_comment (action created) |
if the body matches a configured mention handle: intent-classified → whole-PR action (review / autofix / simplify / adversarial) or a conversational reply run in a PR-branch worktree |
pull_request action labeled |
labels os-review / os-auto-fix / os-simplify / os-adversarial trigger the corresponding behavior; create the labels on your repo first. Auto-fix also merges the current base into conflicting PR branches and resolves the conflicts without force-pushing. |
pull_request opened/reopened/synchronize/ready_for_review |
auto-review, if the PR is non-draft and either carries os-review or the review automation is enabled |
pull_request action closed + merged |
notifies linked sessions; fires the docs-sync automation on github:pr_merged |
pull_request_review |
refreshes PR state; when the Slack agent is enabled, review → Slack notification |
workflow_run |
notifies sessions waiting on a merged PR's deploy |
The webhook secret authenticates GitHub, not the person who caused an event.
Before an event can command the agent, the actor's exact login must appear in
identity.team[].github; the configured policy.githubBotLogins are trusted
separately for machine-originated events. This gate covers PR comments and
inline comments, labels, automatic review events, merge automations, workflow
notifications, Slack review notifications, reconcile retries, and restart
recovery. Unknown actors are ignored. GitHub's author_association field is
not a trust source.
This means a public contributor can still open a PR and receive ordinary credential-free GitHub Actions CI, but cannot wake the Open Session agent, spend its model budget, push code, steer a session, or trigger a privileged PR behavior. Keep the team GitHub roster current; an empty roster fails closed.
Multi-repo: the App webhook covers every repository on which the App is
installed. A repo joins the PR agent when it is also in the config registry
(repos in ~/.opensession/config.json, matched by ghRepo). Events for
unconfigured repos are dropped. Per-PR state, locks, worktrees, and session ids
are repo-qualified for non-default repos (the default repo keeps its historical
bare-number keys). Merge side effects (docs-sync and linked-session deploy
notifications) run for the default repo only.
PR comments, labels, and other event-driven behavior need GitHub to reach the public webhook URL. Configure Public ingress before creating the App so the pre-filled webhook URL is public; if the App already exists, update its webhook URL manually. A private-only instance can reconcile some recent, trusted opted-in PR reviews by polling, but it cannot discover conversational comments or new label commands without webhook delivery.
- Auto-review on every PR push is off by default: the github agent seeds a "review" automation disabled (label-only mode). Enable it in the Automations UI. Not an env var.
- The docs-sync automation is seeded enabled and fires on merge only when
you set a prompt in
integrations.github.docsSyncPrompt. It is an ordinary code automation, so under the current credential policy it can edit its worktree but receives no GitHub token and cannot push or open a PR.integrations.github.docsSyncChannelonly lets the merge handler find and check off a recent Slack message that already links a docs-sync PR; it does not post that announcement itself. integrations.github.shippedChangesChannelsets the default Slack channel in the post-merge Share to Slack composer. It is not an enable switch. A teammate with a personal Slack connection deliberately posts either prose or selected screenshots; channel choices come fromintegrations.slack.channelNames.- Mention replies are always on while the agent is loaded.
- The agent itself is off unless enabled:
integrations.github.enabled: truein config, or theENABLE_GITHUB_AGENTenv flag (which wins when set; see integrations-misc.md). Agent enablement, the webhook secret, and mention handles are read at load time, so restart after changing them.
Prompts and pr-info.ts defaults are config-driven (they interpolate the
default repo's ghRepo, or the PR's own repo when threaded) — no code edits
needed to point the PR agent at your repos.
Ordinary code automations can edit an isolated worktree, but currently receive
neither GH_TOKEN nor GITHUB_TOKEN. Only interactive trusted runs and the
dedicated github-* code workflows receive a user or repository-scoped App
credential. An ordinary automation therefore cannot push or open a GitHub PR.
Its optional prReviewer value is validated, preserved across resume, and
added to the run instructions, but it grants no GitHub authority. Do not rely
on it to publish or surface automation work.
For a PR created by an authorized path, request a GitHub login or org/team
reviewer directly. The reviewer must be a repository collaborator; a requested
team needs access to that repository. GitHub excludes the PR author. Team
requests expand to member logins, then through identity.team to Open Session
people.
pr-review-notifications.ts refreshes the PR cache every 60 seconds. After its
first poll establishes a baseline, it sends web push for newly observed review
requests. Recipients need a web-push subscription, which requires the UI over
HTTPS. A failed or missed push is not retried; removing and later re-requesting
a reviewer creates a new edge. A team request fans out to its members unless
GitHub's own team code-review assignment resolves it to selected people.
Opt-in: interactive sessions open PRs as the actual human who owns the session instead of the bot, and the web UI's name picker becomes a real GitHub sign-in. It is off by default. Without it, team-mode server actions use the App installation identity; single-user mode can use the sole-account flow below without enabling the sign-in gate.
-
Use the same organization-owned GitHub App configured above: tick "Enable Device Flow" and generate a client secret. If the organization restricts GitHub Apps, approve its installation and updated permissions.
Device Flow is not an option here. It is the only sign-in there is, so an app without it refuses every attempt (
device_flow_disabled) and nobody can get in. The callback URL, by contrast, is unused, because sign-in never redirects; put your instance's URL in if GitHub insists on the field. -
Configure
~/.opensession/config.json:{ "integrations": { "github": { "userPrAuth": true, "oauthClientId": "<client id>", "oauthClientSecret": "<client secret>", "appSlug": "<app slug>", "installationOwner": "<organization>" } } }Before setting
userPrAuthdirectly, put at least your own exact GitHub login inidentity.team[].github(and make it an admin when the roster uses explicit admin roles), or every sign-in will be rejected. The Settings UI prevents this lockout when it enables the gate. The private key is stored separately as described above. EnvironmentOPENSESSION_GITHUB_*values win over config. Signing in needs the client id; the secret renews user tokens; the key mints bot installation tokens. -
App and authentication config is read live; no restart is required. Restart only after load-time agent settings change, or once if you want the boot-only
createdByLoginmigration to backfill existing sessions immediately.
What turns on (packages/core/opensession-server/src/server/github-auth.ts, web-auth.ts, routes/auth.ts):
- Sign-in required: the UI shows "Continue with GitHub", which starts the
device flow, the one sign-in every client uses; only logins on
identity.team[].githubmay sign in. Ordinary/api/*calls and the UI WebSocket are 401-gated on the HttpOnly session cookie; non-browser callers useAuthorization: Bearer <token>with a token from~/.opensession/web-sessions.json. Auth routes,/api/health,/live,/ready, client update feeds, and machine routes protected by their own credentials are exceptions. The verified identity overrides client-claimed user names (WS and HTTP), stampscreatedByLoginon new sessions, and a one-time boot migration backfills it onto existing ones. - Organization members imported: after a repository identifies the GitHub
organization, opening the onboarding People step imports up to 10,000
organization members into
identity.team. Existing profile details are preserved, and the import is recorded so removing someone later is not undone on the next page load. - PRs as the owner: signing in also stores the person's GitHub App
user-to-server token (
~/.opensession/github-auth.json, 0600). The App's Members permission lets initial setup list organization members. The runner injects it asGH_TOKEN/GITHUB_TOKENinto interactive, non-least-privilege runs only — automations, unattended kinds, and any run carrying a deny-set stay credential-free. Trusted GitHub code workflows receive the repository-scoped App credential instead. Teammates manage their own connection under Settings → Account.
A simple-mode install is one person on their own box: no operator config, no
separate bot account, no gh auth login, and no sign-in gate. Such a user still
needs their private repos available, to list them in the repo picker, clone
them, and open PRs as themselves. Simple mode connects with a GitHub App you
create, configured entirely in the UI: no file editing, no restart.
- Create the app. The GitHub step in
/welcomesubmits a GitHub App manifest for either a personal account or an organization. The manifest carries the private App name, complete permission set, event subscriptions, and the current public webhook URL when one exists. Confirm Device Flow because its URL parameter is undocumented, then create the App. - Return automatically. GitHub redirects the browser to the private Open Session address with a one-time conversion code. The server exchanges it for the App slug, Client ID, client secret, webhook secret, and private key. It stores the key with mode 0600 and never sends any of those secrets back to the browser. The Use an existing GitHub App disclosure keeps the manual path for an App that was created elsewhere.
- Install on your repositories. Follow the install link and pick the repos to expose. An App credential reaches only repositories included in that installation.
- Connect. Enter the one-time code at
github.com/login/device. The token is stored under the login GitHub reports (~/.opensession/github-auth.json, 0600, never shown again). Interactive HTTPS clones and pushes receive it through a process-local credential helper. No static GitHub token is involved.
A public callback origin is not required for App creation, repository access, or sign-in. When no public ingress exists, the manifest omits its webhook. Configuring Public callbacks later under Settings → Domains and ingress updates the App webhook URL and shared secret with App JWT authentication. This keeps networking out of first-run onboarding while allowing comments, labels, and other webhook events to be enabled later.
The single connected account is the account for this install (there is no roster in simple mode; the one connected account is the acting identity). Disconnect removes it. For a UI-managed App, Remove app then clears the configured client id, slug, secret, private key, and installation intent; the GitHub integration must be disabled first. An App set through environment variables can only be changed by updating those variables and restarting. There is no personal-access-token path: the App is the only simple-mode connect.
Connecting the app does not by itself turn on the sign-in gate (governed
solely by integrations.github.userPrAuth). Because the App's client id is the
same key sign-in reads, graduating a team to
per-user GitHub auth is a
one-flag change, or automatic for an org-owned app: install.sh --org <name>
(or choosing the Organization owner in the wizard) records the org, and at the
connect step rosters the connecting account as the first admin and enables
sign-in in one locked write. A personal app stays single-user with no gate.
deploy/deploy.sh updates a source checkout installed as the system-scope
service. Run it as root on the box, directly or through a root-capable remote
runner such as AWS SSM Run Command:
sudo deploy/deploy.sh # deploy origin/main
sudo deploy/deploy.sh <git-sha> # deploy a specific fetched revisionThere is no deployment workflow in this repository. The script:
- fetches origin and resolves the target without checking out, merging, resetting, or installing dependencies in the shared WIP checkout,
- refuses a target that does not descend from the pinned runtime unless the
operator deliberately sets
OPENSESSION_DEPLOY_ALLOW_DIVERGED=1, - creates or reuses a detached worktree for the exact commit under the deploy
state directory, runs
bun install --frozen-lockfilethere, and verifies its tracked files, - installs the executor and session-kernel credentials and fixed run-host helper, synchronizes all three units and the gateway release environment, and validates the installed helper policy,
- installs the gateway resource override and per-user
opensession.slice, and when Caddy is installed, synchronizes its Tailscale boot-order drop-in, - waits up to
MAX_DRAIN_WAIT(480 seconds by default) foractiveRuns == 0, records the previous release as last-known-good, and atomically switches thecurrentrelease pointer, - stops the gateway before replacing its protocol peer, runs the offline
session migration, restarts and readiness-checks the executor and session
kernel, then restarts the gateway and requires
/readyto recover, and - switches the pointer back and restarts the previous release when a post-switch readiness or health check fails, subject to the kernel schema compatibility floor.
Every source release is one gateway + kernel + executor version, so the root
script rolls out all three even for a frontend-only commit. Detached run-host
units keep executing their original release until their turns finish. For
ordinary source changes that do not update privileged installed artifacts, an
interactive admin can use the lighter deploy_self path described in
self-development.md; it still restarts all three
runtime services.
{ "integrations": { "github": { "enabled": true, "oauthClientId": "Iv…", "oauthClientSecret": "…", "appSlug": "open-session-example", "installationOwner": "your-org", "userPrAuth": true } } }