ADR-0006: GitHub permission tiers and restricted system access ​
Status: accepted (2026-10-12)
Decision ​
- Two GitHub App tiers, recorded in
provider_apps.tier:review: review, triage, replies, assignment, rules import. Never requestscontents: write.fixer: Issue→PR only,contents: writeandpull_requests: write, installed only on repositories that opted in. It subscribes to no events; GitHub still deliversinstallationandinstallation_repositoriesevents to every app, which keeps its installation records current.
- Features are bound to a tier (
featureTier). A feature is available only if an installation of its tier exists and has approved the required permissions; missing permissions are reported as "approval needed" and never requested silently. - Installation tokens are minted with the narrowest
permissionsandrepository_idsa job needs, and cached per scope. BYPASSRLSaccess is not a general shortcut. It is exposed only ascreateSystemDatabase()from@quavon/db/system, whosewithSystem(operation, fn)requires an operation from a closed list (SYSTEM_OPERATIONS:webhook.ingest,setup,installation.lifecycle,outbox.dispatch,maintenance,readiness,auth.session,admin.instance,eval.record,provider.credentials) and records it on a span. Any other name throwsUnknown system operation.auth.sessioncovers GitHub and OIDC sign-in, session revalidation, OIDC group grants, membership sync and MCP access-token validation (looking up the hashed token across tenants, recording its last use; ADR-0018);admin.instancecovers instance-admin routes (GitHub Apps, identity providers, product telemetry, eval runs) and DSAR export and delete;maintenancealso covers product telemetry sending and the outbox age metric.webhook.ingestalso covers the per-connection webhook routes for GitLab, Azure DevOps, Jira, Linear and Azure Boards (connection lookup, delivery row, outbox job); they receive the system database through their route dependencies and do not import@quavon/db/system. Everything else useswithTenant().- Extension operations. An extension (ADR-0015) may add named system operations. They are passed as the third argument of
createSystemDatabase(options, onError, extensionOperations), so they are allowlisted when the connection is created and never later. Each name must match^[a-z][a-z_]*(\.[a-z][a-z_]*)+$and must not be a core name; otherwise creation fails withInvalid extension system operation.withOperations(system, names)narrows an existing system database to extension operations only after checking that each one is allowlisted. The entry points pass them asrunApi({ systemOperations })andrunWorker({ systemOperations }).packages/db/src/rls.int.test.tstests the allowlist and the name rules, and every extension tests each of its operations. - Hosted operations. The hosted edition adds
HOSTED_SYSTEM_OPERATIONSinhosted/db/src/system.ts(saas.tenant,billing.webhook,billing.sandbox_progress,saas.operator_stats), tested inhosted/db/src/rls.int.test.ts; the open core lists none of them. They are used only in the hosted edition (QUAVON_EDITION=enterprise).saas.tenantcovers hosted sign-up (shared app lookup, the public installation check, SMTP settings), owner email verification, and the tenant lifecycle inhosted/worker/src/saas/**: removing unclaimed tenants, uninstalling the app and the hard delete after the grace period.billing.webhookcovers the Mollie webhook: queueing the re-fetch of an object, finding the tenant by our own Mollie customer or invoice id, and thebilling_eventsdedupe; it never trusts the webhook body.billing.sandbox_progresswrites the one aggregate number of the public sandbox progress endpoint topublic_metricsand schedules a deferred re-run; it stores no tenant id.saas.operator_statscovers only the read-only operator overview (GET /api/instance/saas/overview,instance:manage): instance-wide counts and the billing state per tenant, computed on request, never stored, audited on every view (DPA clause 3.5). The billing cycle lists tenant ids through the existingmaintenanceoperation. For the grace reminder emails, the hosted worker extension reads the encryptedsmtprow ofsaas_settingsundermaintenance(hosted/worker/src/extension.ts), as the status monitor does for itssaas_settingsrow; no new operation is added. - An ESLint rule restricts imports of
@quavon/db/system,@quavon/db/dsarand@quavon/hosted-db/systemto allowlisted modules (eslint.config.js). The hosted edition addshosted/db/src/system.ts,hosted/api/src/{main,extension,deps}.ts,hosted/worker/src/{main,extension}.ts,hosted/worker/src/saas/**andhosted/billing/src/{system,jobs}.ts.