Skip to content

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 requests contents: write.
    • fixer: Issue→PR only, contents: write and pull_requests: write, installed only on repositories that opted in. It subscribes to no events; GitHub still delivers installation and installation_repositories events 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 permissions and repository_ids a job needs, and cached per scope.
  • BYPASSRLS access is not a general shortcut. It is exposed only as createSystemDatabase() from @quavon/db/system, whose withSystem(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 throws Unknown system operation. auth.session covers 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.instance covers instance-admin routes (GitHub Apps, identity providers, product telemetry, eval runs) and DSAR export and delete; maintenance also covers product telemetry sending and the outbox age metric. webhook.ingest also 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 uses withTenant().
  • 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 with Invalid 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 as runApi({ systemOperations }) and runWorker({ systemOperations }). packages/db/src/rls.int.test.ts tests the allowlist and the name rules, and every extension tests each of its operations.
  • Hosted operations. The hosted edition adds HOSTED_SYSTEM_OPERATIONS in hosted/db/src/system.ts (saas.tenant, billing.webhook, billing.sandbox_progress, saas.operator_stats), tested in hosted/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.tenant covers hosted sign-up (shared app lookup, the public installation check, SMTP settings), owner email verification, and the tenant lifecycle in hosted/worker/src/saas/**: removing unclaimed tenants, uninstalling the app and the hard delete after the grace period. billing.webhook covers the Mollie webhook: queueing the re-fetch of an object, finding the tenant by our own Mollie customer or invoice id, and the billing_events dedupe; it never trusts the webhook body. billing.sandbox_progress writes the one aggregate number of the public sandbox progress endpoint to public_metrics and schedules a deferred re-run; it stores no tenant id. saas.operator_stats covers 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 existing maintenance operation. For the grace reminder emails, the hosted worker extension reads the encrypted smtp row of saas_settings under maintenance (hosted/worker/src/extension.ts), as the status monitor does for its saas_settings row; no new operation is added.
  • An ESLint rule restricts imports of @quavon/db/system, @quavon/db/dsar and @quavon/hosted-db/system to allowlisted modules (eslint.config.js). The hosted edition adds hosted/db/src/system.ts, hosted/api/src/{main,extension,deps}.ts, hosted/worker/src/{main,extension}.ts, hosted/worker/src/saas/** and hosted/billing/src/{system,jobs}.ts.

The open core is released under the Apache License 2.0.