Aller au contenu principal

App Works — GitHub permissions, tokens and webhook events

Status: Draft · Created: 2026-09-17 · Program: App Works · Closes: EXT-14 Audience: Platform/infra (GitHub App registration, OAuth app, installs), backend (token resolution), security review.

The programme's own permission inventory is APW-02-fork-lifecycle/plan.md §4.5 (plan.md:573-587), which covers read, fork, merge-upstream, sync pull requests, disabling workflows, the Actions switch, pushing a workflow file and webhooks. That table stops there. Several flows in APW-05 (Builds) and APW-09 (upstream pull requests) need permissions and event subscriptions it does not list, and no programme document compares what is asked for against what the platform actually requests today (packages/plugin/src/common/github.scopes.ts). This document is that inventory: one row per step or flow, with the classic OAuth scope, the GitHub App permission, the fine-grained PAT permission, the webhook event subscription, the owning epic and the wave.

Additive-only (R-26, CONTRACTS.md:69). Nothing here removes a scope the platform already requests. Where a permission is already granted and unused by these flows (delete_repo, project), the row says so and keeps it — removing it from the OAuth request would be a subtraction and is not proposed.


0. How to read this document​

ColumnWhat it means
Token / actorWhich credential performs the step. member = the user's own GitHub connection (OAuth or PAT); installation = a GitHub App installation token; platform = a platform-held token.
Classic OAuth scopeThe scope list the platform must have on the acting token (GITHUB_FULL_SCOPES, packages/plugin/src/common/github.scopes.ts:26-35).
GitHub App permissionThe repository/organization permission the App must be granted. Metadata: read is mandatory for every App.
Fine-grained PATThe equivalent fine-grained repository permission.
Webhook eventThe subscription the step needs, if any. — means the step is read- or write-driven and needs no delivery.
Owner · waveThe epic that implements the step and the programme wave it ships in (README.md:302-307).

Source of the permission names. The GitHub App and fine-grained PAT column values are GitHub's own permission names, taken from its permissions references — Permissions required for GitHub Apps and Permissions required for fine-grained personal access tokens. They are recorded here as requirements to check against the live registration, not as a copy of the settings screen: GitHub may rename or regroup a permission, and the registration itself is an operator action (§7). The classic scope strings are quoted from this repository's own source (packages/plugin/src/common/github.scopes.ts:24,26-35).

Two rules that apply to every row:

  1. Token resolution is unchanged. resolvePluginAndToken picks explicit token → platform customer-org PAT when storage is managed → GitHub App installation token → the user's OAuth account → plugin-settings PAT (EXISTING-SUBSTRATE.md:73, APW-02-fork-lifecycle/plan.md:586). This document does not add a resolution path; it says which permissions each step needs on whichever token resolution lands on.
  2. A permission the token lacks is a reportable state, never a silent failure. Missing admin or App permission must end in a named state carrying the permission name, and must never block readiness or sync (APW-02-fork-lifecycle/spec.md:302-303, APW-06-app-runtime/spec.md:264-267).

1. Tokens and actors​

CredentialWho it belongs toWhat it can do hereWhere it is set / resolved
Member OAuth (classic)The signed-in user, granted when they wire a Work to GitHubEverything the member can do: fork, sync, push a workflow, write secrets, open upstream pull requestsGITHUB_FULL_SCOPES (packages/plugin/src/common/github.scopes.ts:26-35); never granted at sign-in — GITHUB_LOGIN_SCOPES is read:user + user:email only (:24)
Member fine-grained PATThe same member, narrowerThe same steps, one repository at a time; refused for GHCR pulls (measured)Member-supplied; the platform validates what a token may do before use (APW-05-builds/plan.md:840-844)
GitHub App installation tokenThe platform's App installed by the member or their orgReads, workflow writes and secret writes inside the installation's repositories — never a member's identityInstallation token fallback in resolvePluginAndToken; App deliveries arrive without a per-repo webhook (APW-05-builds/plan.md:1087-1088)
Platform-held PATThe platform, for a customer organizationReads and writes inside that organization's repositories; used when storage is platform-managedresolvePluginAndToken (EXISTING-SUBSTRATE.md:73) — never used to open an upstream pull request (APW-09-upstream-pull-requests/spec.md:252-256)
Fleet push credentialThe platform, narrowed per repository idcontents: write only — it cannot change workflow files and cannot open a pull request by itselfEXISTING-SUBSTRATE.md:77; APW-02-fork-lifecycle/plan.md:586-587 states this epic never uses it
Per-App-Work image pull tokenThe App Work owner, one token per App WorkPulls one private GHCR package; nothing elseAPW-05-builds/spec.md:326-329, CONTRACTS.md:321

The Fleet push credential, as measured (2026-09-25, APW-08 T17). Its contents: write-only grant is pinned exactly in apps/api/src/fleet/__tests__/fleet-push-credential.service.spec.ts, and both the service and the spec carry a "never add workflows" comment tied to T17: a Fleet node pushes before the platform judges the branch (finalizeRemotePush, then the merge gate re-judges the head), so this grant is what keeps workflow files off the Task branch in the meantime. Protected non-workflow paths and guarded spec blocks can still reach the branch before judgement; they execute nothing. Not yet verified live: that GitHub refuses a workflow-file push from a contents: write installation token. Operator check: mint such a token for a scratch repository in an Ever Works organization, push a commit touching .github/workflows/x.yml, and expect "refusing to allow a GitHub App to create or update workflow". The Task finalize of a cloud (API-side) run (finalizeRun) no longer pushes unjudged: its push is off by default and, when enabled, judged before the push (APW-08 plan §2.5). The agent tool commitToRepo is behind the same switch (off: refused naming FR-12; on: a pre-push check of the call's own files) (THREAT-MODEL.md T-03).


2. Permission matrix — one row per step or flow​

Classic OAuth scopes are named as GitHub names them; repo implies read/write repository content, pull requests and Actions secrets, and repository webhooks also need write:repo_hook on classic tokens. App permissions and fine-grained PAT permissions are the resource names GitHub shows in its own settings screens.

#Step / flowToken / actorClassic OAuth scopeGitHub App permissionFine-grained PATWebhook eventOwner · wave
1Read a repository / upstream: existence, fork network, archived, allow_forking, visibility, size, licence, stars, movedFrommember · installation · platformrepo (private repositories)Metadata: read, Contents: readMetadata: read, Contents: read—APW-02 · W0/P1
2Inspect a pasted URL (ownership, push access, existing fork, Blueprint match, licence preview — no side effects)memberrepo, read:orgMetadata: read, Contents: readMetadata: read, Contents: read—APW-01 · W1
3List the member's organizations for the fork-target pickermemberread:orgMembers: read (organization)Members: read (organization)—APW-01 · W1
4Request a fork into the member's account or organizationmemberrepoAdministration: write (target), Contents: readAdministration: write, Contents: read—APW-02 · W0/P1
5Create a private copy (non-fork duplicate: create repository + push full history)memberrepoAdministration: write, Contents: writeAdministration: write, Contents: write—APW-02 · W1
6Fast-forward a behind-only fork (merge-upstream)memberrepoContents: writeContents: write—APW-02 · W1
7Open / update the sync pull request (ever-works/upstream-sync → tracked branch)memberrepoContents: write, Pull requests: writeContents: write, Pull requests: write—APW-02 · W1
8Disable inherited workflows (Actions hygiene)memberrepo + repository admin roleActions: writeActions: write—APW-02 · W1
9Read / switch the repository Actions switch (GET/PUT /actions/permissions)memberrepo + repository admin roleAdministration: writeAdministration: write—APW-02/05 · W1
10Push a workflow file (.github/workflows/ever-works-build.yml, or the checks-only file)memberworkflowWorkflows: writeWorkflows: write—APW-05 · W1
11Write repository Actions secrets EW_<ENV NAME> for build.args[].fromEnvmemberrepo (Actions secrets are inside repo on classic tokens)Secrets: writeSecrets: write—APW-05 · W1
12Write and then delete the per-run EW_VERIFY__PROMPTED secret (one sealed secret, one run)memberrepoSecrets: writeSecrets: write—APW-05/04 · W1
13Write Actions variables (the existing substrate already does this)memberrepoVariables: writeVariables: write—existing substrate · W0
14Dispatch the build workflow (manual Rebuild; verification run)memberworkflow, repoActions: writeActions: write—APW-05 · W1
15Read run status and jobs (GET /actions/runs/{id}, /jobs) for status, attempt, minutes and the failing stepmemberrepoActions: readActions: readworkflow_run (see §3)APW-05 · W1
16Download the ever-works-build-result artifact and read a failed run's log tailmemberrepoActions: readActions: read—APW-05 · W1
17Read check runs for the R-9 checks (Ever Works check: {name})memberrepoChecks: readChecks: readcheck_run (see §3)APW-08/05 · W1
18Read package visibility and the pushed image's digest (HEAD /v2/<name>/manifests/... on GHCR + the registry token endpoint)member (classic PAT)read:packages (not in GITHUB_FULL_SCOPES today)Packages: readPackages: read—APW-05/06 · W1
19Validate and use the per-App-Work image pull token (private images)member (classic PAT)read:packages only — the platform refuses anything broaderPackages: readRefused — fine-grained tokens were measured to fail GHCR pulls (APW-05-builds/plan.md:840-844)—APW-05 · W1
20Install / update the repository webhook (workflow_run only)memberwrite:repo_hookWebhooks: writeWebhooks: writethe hook it installs: workflow_runAPW-05/02 · W1
21Open an upstream pull request from the member's fork into the upstreammember only — a user token, never an installation or platform tokenrepo (+ read:org when the fork lives in an organization)Not usable: an installation token is forbidden by FR-24Contents: read (fork + upstream), Pull requests: write—APW-09 · W2
22Prepare the upstream branch in the fork (upstream-pr/{slug}-{4 hex}, one commit, no sign-off)memberrepoContents: writeContents: write—APW-09 · W2
23Poll upstream pull-request status, checks and reviews (upstream sends the platform no events)memberrepoPull requests: read, Checks: readPull requests: read, Checks: read— (polling by design, APW-09-upstream-pull-requests/spec.md:276-278)APW-09 · W2
24Push an approved review follow-up to the fork branch; delete the preparation branch on withdrawmemberrepoContents: writeContents: write—APW-09 · W2
25Read a runtime catalog (ever-works/templates, ever-works/platforms, Blueprint repositories) — tokenless first, authenticated fallbackplatform · installationrepo only when the fallback is usedMetadata: read, Contents: readMetadata: read, Contents: read—APW-03/11 · W1/P2
26Delete the fork or private copy when the member explicitly asks (typed owner/name, admin permission required)memberdelete_repoAdministration: writeAdministration: write—APW-01 · W1
27Receive App-level deliveries: installation and repository-set changes, and pushinstallation (App webhook)—no extra permission (App-level webhook)—installation, installation_repositories, push (apps/api/src/ingest/github/github-webhook-dispatcher.service.ts:53)existing substrate · W0
28Open the upstream pull request from the member's fork and push review follow-ups (added 2026-09-17, APW-09)member (a user token, never an installation token)repo (+ public_repo for a public upstream)Contents: write, Pull requests: writeContents: write, Pull requests: write—APW-09 · W2
29Read the repository's interaction limit — the temporary control APW-09's collaboratorsOnly refusal is derived frommemberrepoto be confirmed (APW-09 T5 spike (d) reads it with a non-admin token)to be confirmed—APW-09 · W2
30Read the cross-repository diff the preparation verifier checks (GET /repos/{upstream}/compare/{base}...{forkOwner}:{branch})memberrepo (a public upstream needs no token)Contents: readContents: read—APW-09 · W2
31Read the maintainer opt-out marker (repository topics and a file the project documents, APW-09 FR-40)memberrepoMetadata: read, Contents: readMetadata: read, Contents: read—APW-09 · W2
28Detect a repository renamed, transferred, archived or deletedany (read path)repoMetadata: readMetadata: readrepository (actions: renamed, transferred, deleted, archived) — not subscribed today; the platform currently learns a rename from the provider redirect (movedFrom, APW-02-fork-lifecycle/plan.md:533-535)APW-02/03 · W1

Rows 11, 12, 15, 16, 17, 18 and 21 are the EXT-14 delta — they are absent from APW-02-fork-lifecycle/plan.md §4.5 and from packages/plugin/src/common/github.scopes.ts.

One discrepancy recorded rather than papered over. APW-02-fork-lifecycle/plan.md §4.5 lists Actions: write for disabling a workflow, while the same plan's plugin notes record that GitHub answers a refused disable with a missing-permission error naming administration for App tokens and actions otherwise (plan.md:554-558). Both are quoted here as written; the App registration must satisfy the permission GitHub actually checks, and the owning epic — not this document — should settle which name the matrix keeps. Nothing here edits either file.


3. Webhook event subscriptions​

The receiver is the existing POST /api/ingest/github/events, verified per delivery against the correct install's webhook secret. Consumers register their own event lists on the dispatcher; adding a consumer does not change the receiver.

EventActions the programme relies onConsumer / purposeSubscription status todayOwner · wave
workflow_runrequested, in_progress, completedAPW-05's new consumer creates and updates a Build by run id; the existing intake keeps normalising fields, it drops non-completed deliveries and stays as it isConsumed already (apps/api/src/ingest/github/github-check-intake.service.ts:40, :449); APW-05 registers a second consumer (APW-05-builds/plan.md:1054-1071) and installs a per-repository hook for latency (:1081-1106)APW-05 · W1
check_runcreated, completed, rerequestedCheck-run results for the R-9 Ever Works check: {name} runs and the Task CI auto-resume pathConsumed already (github-check-intake.service.ts:40); APW-08 reads check runs for its quality gates (R-9, CONTRACTS.md:52; APW-08-evolve-loop/spec.md:247-248)APW-08/05 · W1
check_suitecompletedRoll-up signal alongside check_runConsumed already (github-check-intake.service.ts:40)existing substrate · W0
pull_requestopened, synchronize, reopened, closedTask PR lifecycle, merge detection, delivery chainHandled at the shared receiver (EXISTING-SUBSTRATE.md:70)APW-08 · W1
pushdefault branchSpec re-evaluation trigger, App-level installation syncHandled (github-webhook-dispatcher.service.ts:53, APW-03-app-spec-and-catalog/spec.md:228-230)APW-03 · W1
installation, installation_repositoriescreated, deleted, added, removedGitHub App installation / repository-set syncHandled (github-webhook-dispatcher.service.ts:53)existing substrate · W0
issues, dependabot_alert—Community PR / incident intake (unchanged by this programme)Handled (apps/api/src/ingest/github/github-issue-intake.service.ts:209)existing substrate · W0
repositoryrenamed, transferred, deleted, archived, unarchivedClosing the gap between "the platform notices on the next read" and "the platform is told"Not subscribed. The sync path already follows a renamed default branch (APW-02-fork-lifecycle/spec.md:333) and detects a moved repository through the provider redirect, so this is a latency/robustness item, not a correctness itemAPW-02 · proposed

Why workflow_run and not push. APW-05 installs exactly workflow_run on the user's repository: push would duplicate the sync leg and pull_request would start a second review loop (APW-05-builds/plan.md:1093-1096). Discovery by polling is what makes Builds correct; the hook only makes them fast (:1083-1085).


4. The delta against what the platform requests today​

packages/plugin/src/common/github.scopes.ts exports two scope sets and one backward-compatible alias:

  • GITHUB_LOGIN_SCOPES = ['read:user', 'user:email'] (:24) — sign-in only, deliberately no repo access.
  • GITHUB_FULL_SCOPES = ['user:email', 'read:user', 'repo', 'delete_repo', 'workflow', 'write:repo_hook', 'read:org', 'project'] (:26-35) — the broad set the capability flows request.
  • GITHUB_SCOPES = GITHUB_FULL_SCOPES (:45) — the plugin's default-scope fallback when no caller supplies a list.
Scope / permissionRequested todayNeeded by App Works forDelta
read:user, user:emailyes — GITHUB_LOGIN_SCOPES:24nothing beyond identitynone
repoyes — :29rows 1–9, 11–17, 21–26 (content, pull requests, hooks, Actions secrets)none — sufficient for classic OAuth
workflowyes — :31row 10, pushing a workflow filenone
write:repo_hookyes — :32row 20, installing the workflow_run hooknone
read:orgyes — :33rows 2, 3, 21 (organization fork targets; organization-owned forks)none
delete_repoyes — :30row 26 only — the explicit, typed, admin-checked fork deletion (APW-01-app-work-kind/spec.md:426-428)Already granted and used by one flow; kept as-is (removing it would be a subtraction, R-26)
projectyes — :34no App Works flow uses itAlready granted, unused here; kept as-is (R-26). Recorded so nobody "finds" it later and assumes it is load-bearing
read:packagesno — absent from GITHUB_FULL_SCOPESrows 18 and 19 — GHCR visibility, the digest check, and the per-App-Work pull tokenGap. Required on a classic token; the platform already validates for it (APW-05-builds/plan.md:840-844)
App Secrets: writeno — not in any programme document before this onerows 11, 12 — EW_<ENV NAME> and EW_VERIFY__PROMPTEDGap. An App installation needs this permission granted and re-approved (§5)
App Actions: readnorows 15, 16 — run status, jobs, logs, artifact downloadGap. The existing Actions service can dispatch and list workflows but has no run/artifact read (packages/plugins/github/src/github-actions.service.ts:194-214 returns void for a dispatch)
App Checks: readnorow 17 — the R-9 check runsGap. The read path exists (packages/plugins/github/src/github-api.service.ts:976-995, checks.listForRef) and reports an incomplete roll-up rather than a false green
App Packages: readnorows 18, 19Gap
App Variables: writeno (existing substrate writes variables)row 13Existing-substrate gap, listed so the App registration is complete in one pass
Fine-grained: Secrets, Actions, Checks, Packagesnorows 11–19Gap — the fine-grained equivalents of the four App permissions above
repository webhook eventnorename / transfer / archive / delete awareness (§3)Gap, optional — the flows work through the provider redirect today

The connection-scope presets (packages/plugins/github/src/github.connection-scopes.ts:24-40) already state the rule this table exists to serve: provider permissions are listed "so the platform can tell whether widening to 'Read and write' needs the owner to re-approve the connected account", with read = read:user + user:email + repo + read:org and write = all of GITHUB_FULL_SCOPES (:24-40).


5. Permission growth and the re-approval GitHub requires​

Permissions are not silently upgradeable. When an App's or a token's access grows, the party who owns that access must approve the growth; until they do, the affected step fails and the platform must say so by name rather than degrade invisibly.

Growth eventWho must re-approveWhat the member / operator seesPlatform obligation
A GitHub App gains a permission (e.g. Secrets: write, Actions: read, Checks: read, Packages: read)Every installation owner of that App, in each environment where it is installedGitHub's own review prompt lists the new permissions; the App keeps its previous access until approvedThe affected step must answer a named missing-permission state and never block the flows that do not need it (APW-02-fork-lifecycle/spec.md:302-303, APW-06-app-runtime/spec.md:264-267; ACC-02-20, ACC-06-05)
A classic OAuth app gains a scope (e.g. read:packages)Every member who connected GitHub — existing tokens keep their old scopesThe next request that needs the new scope is refused; the member must reconnect to grant itClassify as a permission problem, not a generic error (APW-02-fork-lifecycle/plan.md:524), and state what to do — reconnect (the same copy path as ACC-01-15, "Revoking GitHub mid-fork … Reconnect")
A fine-grained PAT is narrowed or expiresThe member, by editing the tokenRows 18/19 refuse; a pull token expiring within 14 days warns on the Builds and Deploy tabsValidate what a token may do before using it; refuse anything broader than the step needs (APW-05-builds/spec.md:326-330)
The repository Actions switch is turned off by the repository ownerThe member (their own repository)Builds read Blocked with the reason; Actions hygiene must never switch Actions off for the repository as a wholeAPW-05-builds/spec.md:238-239, APW-02-fork-lifecycle/spec.md:295
A branch protection rule appears (reviews or status checks required)The member (their own repository)The workflow is proposed as one pull request instead of a direct commit; at most one such pull request is openAPW-05-builds/spec.md:221-224

GitHub's documented behaviour behind the first row (Approving updated permissions for a GitHub App): GitHub notifies the owner of an account the App is installed on, the new permissions are reviewable and refusable, and a refusal leaves the App with its current permissions — so a flow that needs the new permission fails until it is granted, which is exactly why the platform must answer a named permission state rather than a generic error. For an App that is authorized but not installed, or for account-level permissions, GitHub does not notify at all and the App must prompt the member to re-authorize.

Installation-token caveat. Re-approval changes what an installation token can do, but it never changes whose identity a step acts under: opening an upstream pull request remains a member-connection-only operation (APW-09-upstream-pull-requests/spec.md:252-256), and the Fleet push credential's contents: write cannot grow into workflow writes (EXISTING-SUBSTRATE.md:77).


6. Environments​

EnvironmentGitHub estate used by the programmeWhat is switched on / offGrounded in
devA test tenant inside Ever Works with its own connected GitHub account; fixture upstreams owned by ever-worksEVER_WORKS_E2E_FAKES set in the PR lanes only; catalog ref may pin the test-only e2e branch of the listing repositoryACCEPTANCE.md:74-90, ACCEPTANCE.md:80, ACCEPTANCE.md:102
stageThe same tenant model, plus the managed-tier trialEVER_WORKS_APPS_MANAGED_ENABLED may be true here and nowhere else; EVER_WORKS_APPS_MAX_SCOPE stays verified-blueprints until Wave 3ACCEPTANCE.md:74-79
productionReal member connections and real App Work repositoriesEVER_WORKS_E2E_FAKES unset; the catalog ref pinned to a SHA or tag; works-app and EVER_WORKS_APP_WORKS_ENABLED fail closedCONTRACTS.md:437, CONTRACTS.md:416, CONTRACTS.md:412-413
All threeOne GitHub App registration per environment, each with its own installations and its own permission reviewA permission increase is approved per installation, per environment — approving it on stage does not approve it on productionProposed — see §7; the environment-parity rule for flags is already in ACCEPTANCE.md:74-80

7. Open items and shared-file requests​

  1. read:packages is not in GITHUB_FULL_SCOPES. Rows 18 and 19 need it on a classic token, and the platform already validates a pull token for exactly that scope (APW-05-builds/plan.md:840-844). Proposal for the lead: add read:packages to the capability scope set in a follow-up change owned by APW-05 — additively, with the existing delete_repo and project entries left exactly as they are (R-26). Nothing in this document changes that file.
  2. The App registration list needs an owner. The four App permissions in rows 11–19 and the fine-grained equivalents must exist on the App before the corresponding steps can pass; registering them is an operator action, and the re-approval consequences are in §5. The registration itself is not a spec artifact.
  3. repository event subscription (§3) is proposed, not required: the redirect-following read path already keeps renames and transfers correct, so this is a latency improvement that should not gate Wave 1.
  4. Per-environment App registrations (§6, last row) is stated as Proposed: no epic says whether dev/stage share a registration with production. It belongs in the private operations repository once decided, with the pointer recorded in APW-02-fork-lifecycle/plan.md §4.5's successor table.
  5. Plan cross-references — APW-02-fork-lifecycle/plan.md:573-587, APW-05-builds/plan.md and APW-09-upstream-pull-requests/plan.md should each link this document from their permission/inventory sections. Those files are named in the EXT-14 row as targetFiles but are outside this document's write scope; the literal one-line additions are supplied to the lead in the accompanying shared-file request.
  6. The Actions: write vs administration discrepancy in §2's note should be settled in APW-02-fork-lifecycle/plan.md §4.5 by whoever owns that file — it decides which permission the App registration must be granted before Actions hygiene can work at all.
  7. Row 29's permission is unconfirmed (added 2026-09-17). GitHub documents the interaction-limits read as admin-scoped, but APW-09's eligibility runs on the member's token, so whether a non-admin member can read it at all — and whether a 403 must read as "cannot tell" rather than "no limit" (which APW-09/plan.md §4 now requires) — is what APW-09 T5's spike (d) answers. Until it runs, row 29 says so rather than guessing, and the refusal it backs stays unverified.

8. Revision log​

DateAuthorChange
2026-09-17App Works program (EXT-14)First version: tokens and actors, 28-step permission matrix (classic OAuth · App permission · fine-grained PAT · event · owner · wave), webhook subscriptions, the delta against github.scopes.ts, re-approval rules and the environment split.

Related documents. Program README · CONTRACTS · ACCEPTANCE · EXISTING-SUBSTRATE · THREAT-MODEL · APW-02 plan §4.5 · APW-05 plan · APW-09 plan · spec-tree verifier