Skip to content

fix(orb): installation/permission/config integrity — suspension backfill, broker identity, numeric env validation, migration content hash#9222

Merged
JSONbored merged 5 commits into
mainfrom
fix/9151-9152-9157-9164-install-permission-integrity
Jul 27, 2026
Merged

fix(orb): installation/permission/config integrity — suspension backfill, broker identity, numeric env validation, migration content hash#9222
JSONbored merged 5 commits into
mainfrom
fix/9151-9152-9157-9164-install-permission-integrity

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

Changes

  • src/orb/app-auth.ts / src/orb/installations.ts: parse and write through suspended_at in the backfill instead of hardcoding NULL. removed_at still clears — GitHub's installations list never reports an actually-uninstalled App, so every install this loop sees is by definition still installed (documented inline).
  • src/github/app.ts: mintInstallationToken's broker branch now throws when the broker's claimed installationId is non-zero and differs from the requested one, instead of silently caching/returning that token under the caller's id. The permissions-persist step is no longer gated on an installationId match that a legacy zero-default reply could never satisfy (a second latent bug the same code shape hid). getRepositoryCollaboratorPermission now prefers role_name over permission, falling back to permission only when role_name is absent.
  • src/selfhost/preflight.ts: new positiveInteger helper; CRON_INTERVAL_MS, PORT, and GITHUB_CACHE_TTL_SECONDS are validated at boot and fail loudly (not silently coerced) when set to a non-integer, unit-suffixed, or out-of-range value. src/selfhost/cron-alignment.ts gets a shared CRON_INTERVAL_MIN_MS floor constant. src/server.ts's three call sites now read through the existing parsePositiveIntEnv (queue-common.ts) as runtime defense-in-depth (floors/clamps with a warning instead of coercing to NaN).
  • src/selfhost/migrate.ts: _selfhost_migrations gains a content_sha256 column, populated on apply and checked against the current on-disk hash of every already-applied file on every boot. A mismatch logs loudly and fails boot (thrown) instead of being silently skipped. A pre-existing row with no stored hash is backfilled to its current content as the baseline.

Judgment calls

  • No down-migration path is added for orb(selfhost): the migration ledger records only a filename — a migration edited after it was applied is undetectable, and a half-applied file is recorded as complete #9164 — this repo's migrations remain forward-only, same as every existing migrations/*.sql file; stated explicitly in the function's doc comment per the issue's own ask.
  • The four grandfathered duplicate-number pairs (0015/0017/0074/0156) need no special handling for the content-hash check: the ledger — and this drift check — key on filename, not the leading number, so each grandfathered file already has its own independent row/hash like any other migration. Documented inline.
  • No new numbered migrations/NNNN_*.sql file for orb(selfhost): the migration ledger records only a filename — a migration edited after it was applied is undetectable, and a half-applied file is recorded as complete #9164's content_sha256 column: _selfhost_migrations is a purely self-host runtime table, created and evolved entirely inside migrate.ts (CREATE TABLE IF NOT EXISTS / ALTER TABLE ... ADD COLUMN, tolerating "already exists" like the rest of the file) — confirmed via search, it has never had a numbered migration or a schema.ts entry, since Cloudflare D1 never runs this code path. Adding one would create an unused table in the cloud DB and duplicate the table's actual, working self-managed-schema mechanism.
  • CRON_INTERVAL_MS=0 is explicitly rejected, not treated as "disable the cron" — there's no supported way to run this entrypoint without its maintenance cron (sweep dispatch, ops-alerts, reconciliation, registry refresh), so 0 is just another invalid value.
  • Bounds for the new numeric checks (PORT 1–65535, GITHUB_CACHE_TTL_SECONDS 0–86400, CRON_INTERVAL_MS 10s–24h) are reasonable defaults not spelled out in the issue.

Deferrals / already-fixed check

All four issues were reproduced against current main and none were already fixed by a recent merge.

Validation

  • npx tsc --noEmit -p tsconfig.json --incremental false — clean
  • Targeted test runs (all passing): test/unit/orb-app-auth.test.ts, test/integration/orb-installations.test.ts, test/unit/github-app.test.ts, test/unit/auth-per-repo-admin.test.ts, test/unit/selfhost-preflight.test.ts, test/unit/selfhost-cron-alignment.test.ts, test/unit/selfhost-migrate.test.ts — 166 tests
  • npm run test:changed — 449 files / 11025 tests passed; the only 4 failures (orb-ingest.test.ts, orb-onboarding.test.ts, orb-relay.test.ts ×2) are pre-existing on origin/main without this branch's changes (verified via git stash), unrelated to this PR
  • npm run test:coverage (unsharded) — not run locally per task instructions; CI's codecov/patch will report
  • npm run ui:* — no UI changes in this PR
  • No migrations/schema changes in this PR (see judgment calls above)

Safety

Closes #9151
Closes #9152
Closes #9157
Closes #9164

…consent (#9151)

listOrbAppInstallations now parses suspended_at from GitHub's installation
list, and backfillOrbInstallations writes it through instead of hardcoding
NULL on every upsert. Previously a suspension recorded by the
installation.suspend webhook was silently cleared by the next backfill,
making the broker's suspended_at eligibility check always-passing.

removed_at stays cleared: GitHub's installations list never reports an
actually-uninstalled App, so every install this loop sees is by definition
still installed.
…aborator role (#9152)

mintInstallationToken now throws when a brokered token's installationId is
non-zero and differs from the requested install, instead of caching and
returning it under the caller's id — closing a cross-tenant token-serving
path a stale caller-side installationId (e.g. after an App
uninstall/reinstall) could reach. The permissions write is no longer gated
on an installationId match that a legacy zero-default reply could never
satisfy.

getRepositoryCollaboratorPermission now prefers the collaborator-permission
endpoint's role_name over its coarse permission field, since permission only
ever resolves to admin/write/read/none — collapsing Maintain into "write"
and Triage into "read" and making the "maintain" tier unreachable at both
isPerTenantAdmin and resolveRealRepoPermissionAssociation.
…a bad value NaN through (#9157)

preflightEnv now hard-fails boot when CRON_INTERVAL_MS, PORT, or
GITHUB_CACHE_TTL_SECONDS is set to a non-plain-integer or out-of-range
value, via a new positiveInteger check. Previously a bare
Number(process.env.X ?? default) silently turned a unit-suffixed or
malformed value into NaN, which setTimeout/setInterval coerce to a 1ms
delay (spinning the cron at ~1000 ticks/second) or which a `> 0` gate
always fails (silently disabling the GitHub response cache).

server.ts's three call sites now also read through parsePositiveIntEnv
(queue-common.ts) as runtime defense-in-depth, floored/clamped with a
warning instead of coercing to NaN. CRON_INTERVAL_MS=0 is explicitly NOT
treated as "disable the cron" — there is no supported way to run this
entrypoint without its maintenance cron.
…a a content hash (#9164)

The self-host migration ledger (_selfhost_migrations) now records each
applied file's content_sha256 alongside its name. On every boot,
runSelfHostMigrations recomputes the current on-disk hash of every
already-applied file and compares it to the recorded one: a mismatch fails
boot loudly (logged, then thrown) instead of the prior behavior, where the
ledger's filename-only key made an in-place edit after apply invisible
forever.

A pre-#9164 row with no stored hash is backfilled to its current content as
the baseline, since the original apply-time content can't be recovered. No
down-migration path is added — this repo's migrations remain forward-only.
@JSONbored JSONbored self-assigned this Jul 27, 2026
@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

…ations

npm run selfhost:env-reference was not re-run after CRON_INTERVAL_MS/PORT/
GITHUB_CACHE_TTL_SECONDS validation moved into preflight.ts (#9157).
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 27, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
loopover-ui d4cffca Commit Preview URL

Branch Preview URL
Jul 27 2026, 07:51 AM

@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 36 bytes (0.0%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
loopover-ui 7.43MB 36 bytes (0.0%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: loopover-ui

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/add-scalar-classes-Cf9AbH3o.js (New) 2.17MB 2.17MB 100.0% 🚀
assets/tanstack-vendor-AP0TueuY.js (New) 803.86kB 803.86kB 100.0% 🚀
assets/docs.fumadocs-spike-api-reference-Dy-83ekW.js (New) 442.88kB 442.88kB 100.0% 🚀
assets/AgentScalarChatInterface.vue-B71bsT07.js (New) 201.71kB 201.71kB 100.0% 🚀
assets/modal-DDGlOYav.js (New) 184.39kB 184.39kB 100.0% 🚀
assets/client-CWIGjSRg.js (New) 146.06kB 146.06kB 100.0% 🚀
assets/self-hosting-configuration-Befldias.js (New) 101.6kB 101.6kB 100.0% 🚀
assets/maintainer-panel-QeJ45iks.js (New) 79.0kB 79.0kB 100.0% 🚀
assets/routes-99LpxxrV.js (New) 35.77kB 35.77kB 100.0% 🚀
assets/owner-panel-BlvSERyf.js (New) 27.52kB 27.52kB 100.0% 🚀
assets/app-BymTp6KX.js (New) 25.78kB 25.78kB 100.0% 🚀
assets/ui-vendor-iVpPzx--.js (New) 22.28kB 22.28kB 100.0% 🚀
assets/miner-panel-CQznKIL6.js (New) 20.24kB 20.24kB 100.0% 🚀
assets/app.runs-Dh6jXJqL.js (New) 20.22kB 20.22kB 100.0% 🚀
assets/api._op-DE1BL7kT.js (New) 17.57kB 17.57kB 100.0% 🚀
assets/self-hosting-docs-audit-BC34eLhy.js (New) 16.6kB 16.6kB 100.0% 🚀
assets/docs._slug-Be3UwUYx.js (New) 15.37kB 15.37kB 100.0% 🚀
assets/playground-panel-WTEQ2MdO.js (New) 14.43kB 14.43kB 100.0% 🚀
assets/fairness-D9gK2DgW.js (New) 10.6kB 10.6kB 100.0% 🚀
assets/app.audit-_CqsIBKu.js (New) 10.08kB 10.08kB 100.0% 🚀
assets/app.config-generator-DvSIGuYM.js (New) 10.06kB 10.06kB 100.0% 🚀
assets/maintainers-DkxBcxuc.js (New) 8.06kB 8.06kB 100.0% 🚀
assets/miners-BmceDscj.js (New) 7.91kB 7.91kB 100.0% 🚀
assets/agents-kSVMY__v.js (New) 7.74kB 7.74kB 100.0% 🚀
assets/commands-panel-Canca6sX.js (New) 6.65kB 6.65kB 100.0% 🚀
assets/maintainer-workflow-DL7M2qOg.js (New) 6.52kB 6.52kB 100.0% 🚀
assets/digest-panel-C11cw9oy.js (New) 6.15kB 6.15kB 100.0% 🚀
assets/repos._owner._repo.quality-BTq-K_VF.js (New) 6.14kB 6.14kB 100.0% 🚀
assets/docs-nav-UEHgM9kT.js (New) 5.95kB 5.95kB 100.0% 🚀
assets/docs.index-D7pTVrSg.js (New) 5.95kB 5.95kB 100.0% 🚀
assets/api.index-E1oJfyBh.js (New) 4.7kB 4.7kB 100.0% 🚀
assets/docs-BRIu7j4W.js (New) 2.7kB 2.7kB 100.0% 🚀
assets/api-Dmn22gPI.js (New) 2.69kB 2.69kB 100.0% 🚀
assets/docs-page-CY2UJeu6.js (New) 2.1kB 2.1kB 100.0% 🚀
assets/table-OGI4S3_W.js (New) 1.75kB 1.75kB 100.0% 🚀
assets/app.workbench-CA6183eh.js (New) 1.58kB 1.58kB 100.0% 🚀
assets/tabs-CJWVgTGA.js (New) 1.39kB 1.39kB 100.0% 🚀
assets/app.repos-B1E9fn9Y.js (New) 1.07kB 1.07kB 100.0% 🚀
assets/input-qqx3U2Bi.js (New) 796 bytes 796 bytes 100.0% 🚀
assets/file-cog-CiLvefOA.js (New) 758 bytes 758 bytes 100.0% 🚀
assets/app.maintainer-Cb1p7aWP.js (New) 502 bytes 502 bytes 100.0% 🚀
assets/app.owner-B9kBQyW1.js (New) 474 bytes 474 bytes 100.0% 🚀
assets/app.commands-B7Xj69cD.js (New) 455 bytes 455 bytes 100.0% 🚀
assets/app.playground-BDdO0DHa.js (New) 442 bytes 442 bytes 100.0% 🚀
assets/index-BQ1D46A_.js (New) 438 bytes 438 bytes 100.0% 🚀
assets/app.digest-Bpza_47A.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/eye-off-toGLBRxx.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/app.miner--TtLeI8t.js (New) 422 bytes 422 bytes 100.0% 🚀
assets/key-round-wjnZCqYM.js (New) 355 bytes 355 bytes 100.0% 🚀
assets/bot-Cm_O3LFj.js (New) 328 bytes 328 bytes 100.0% 🚀
assets/trash-2-DYixLXkU.js (New) 328 bytes 328 bytes 100.0% 🚀
assets/save-CPfYPT46.js (New) 327 bytes 327 bytes 100.0% 🚀
assets/git-pull-request-arrow-BsG0FOqb.js (New) 321 bytes 321 bytes 100.0% 🚀
assets/list-checks-t55NB8Dm.js (New) 279 bytes 279 bytes 100.0% 🚀
assets/compass-Big9PXPb.js (New) 251 bytes 251 bytes 100.0% 🚀
assets/history-P2nDU39B.js (New) 237 bytes 237 bytes 100.0% 🚀
assets/message-square-BC7gEn_U.js (New) 233 bytes 233 bytes 100.0% 🚀
assets/lock-B3mwZfV4.js (New) 206 bytes 206 bytes 100.0% 🚀
assets/rotate-cw-Bj-8zF9o.js (New) 201 bytes 201 bytes 100.0% 🚀
assets/play-DoJAJzxk.js (New) 190 bytes 190 bytes 100.0% 🚀
assets/circle-check-ChQ0O352.js (New) 178 bytes 178 bytes 100.0% 🚀
assets/search-Ccl-znKw.js (New) 174 bytes 174 bytes 100.0% 🚀
assets/add-scalar-classes-BKPy5i30.js (Deleted) -2.17MB 0 bytes -100.0% 🗑️
assets/tanstack-vendor-CyzMeffK.js (Deleted) -803.86kB 0 bytes -100.0% 🗑️
assets/docs.fumadocs-spike-api-reference-C9hXvbar.js (Deleted) -442.88kB 0 bytes -100.0% 🗑️
assets/AgentScalarChatInterface.vue-Efv34OpK.js (Deleted) -201.71kB 0 bytes -100.0% 🗑️
assets/modal-DwcF4TCu.js (Deleted) -184.39kB 0 bytes -100.0% 🗑️
assets/client-DR9LNdD7.js (Deleted) -146.06kB 0 bytes -100.0% 🗑️
assets/self-hosting-configuration-PpkhUKH8.js (Deleted) -101.56kB 0 bytes -100.0% 🗑️
assets/maintainer-panel-IANfueob.js (Deleted) -79.0kB 0 bytes -100.0% 🗑️
assets/routes-DM07_qfb.js (Deleted) -35.77kB 0 bytes -100.0% 🗑️
assets/owner-panel-Zcn_9i5w.js (Deleted) -27.52kB 0 bytes -100.0% 🗑️
assets/app-DGdXTejt.js (Deleted) -25.78kB 0 bytes -100.0% 🗑️
assets/ui-vendor-Bf7XULf0.js (Deleted) -22.28kB 0 bytes -100.0% 🗑️
assets/miner-panel-CR--TGU3.js (Deleted) -20.24kB 0 bytes -100.0% 🗑️
assets/app.runs-DDWYImR0.js (Deleted) -20.22kB 0 bytes -100.0% 🗑️
assets/api._op-BfMGcrgs.js (Deleted) -17.57kB 0 bytes -100.0% 🗑️
assets/self-hosting-docs-audit-8psuoTR1.js (Deleted) -16.6kB 0 bytes -100.0% 🗑️
assets/docs._slug-CHZ71lqA.js (Deleted) -15.37kB 0 bytes -100.0% 🗑️
assets/playground-panel-qR4LWDDV.js (Deleted) -14.43kB 0 bytes -100.0% 🗑️
assets/fairness-vw1gGqgJ.js (Deleted) -10.6kB 0 bytes -100.0% 🗑️
assets/app.audit-sG6W_q6x.js (Deleted) -10.08kB 0 bytes -100.0% 🗑️
assets/app.config-generator-DJ6Alt5j.js (Deleted) -10.06kB 0 bytes -100.0% 🗑️
assets/maintainers-DED5yMWG.js (Deleted) -8.06kB 0 bytes -100.0% 🗑️
assets/miners-BU-qMUh6.js (Deleted) -7.91kB 0 bytes -100.0% 🗑️
assets/agents-B7RL3EDi.js (Deleted) -7.74kB 0 bytes -100.0% 🗑️
assets/commands-panel-DVBK7vzW.js (Deleted) -6.65kB 0 bytes -100.0% 🗑️
assets/maintainer-workflow-BYMzSWCv.js (Deleted) -6.52kB 0 bytes -100.0% 🗑️
assets/digest-panel-BZsN56Yw.js (Deleted) -6.15kB 0 bytes -100.0% 🗑️
assets/repos._owner._repo.quality-lXB8ApO_.js (Deleted) -6.14kB 0 bytes -100.0% 🗑️
assets/docs-nav-S3xSHTh6.js (Deleted) -5.95kB 0 bytes -100.0% 🗑️
assets/docs.index-CE-sS6M-.js (Deleted) -5.95kB 0 bytes -100.0% 🗑️
assets/api.index-FyBQTZjE.js (Deleted) -4.7kB 0 bytes -100.0% 🗑️
assets/docs-CNixBTK7.js (Deleted) -2.7kB 0 bytes -100.0% 🗑️
assets/api-BCTFLF3h.js (Deleted) -2.69kB 0 bytes -100.0% 🗑️
assets/docs-page-hskwdDVZ.js (Deleted) -2.1kB 0 bytes -100.0% 🗑️
assets/table-DdkKYdHO.js (Deleted) -1.75kB 0 bytes -100.0% 🗑️
assets/app.workbench-7LpgKB-7.js (Deleted) -1.58kB 0 bytes -100.0% 🗑️
assets/tabs-Dsy0Dew9.js (Deleted) -1.39kB 0 bytes -100.0% 🗑️
assets/app.repos-CB_Mt78v.js (Deleted) -1.07kB 0 bytes -100.0% 🗑️
assets/input-BB0YAVQ2.js (Deleted) -796 bytes 0 bytes -100.0% 🗑️
assets/file-cog-WtxTdKMp.js (Deleted) -758 bytes 0 bytes -100.0% 🗑️
assets/app.maintainer-BftPwMCE.js (Deleted) -502 bytes 0 bytes -100.0% 🗑️
assets/app.owner-B0efVq7j.js (Deleted) -474 bytes 0 bytes -100.0% 🗑️
assets/app.commands-BnXzorwC.js (Deleted) -455 bytes 0 bytes -100.0% 🗑️
assets/app.playground-DBx8lzca.js (Deleted) -442 bytes 0 bytes -100.0% 🗑️
assets/index-DMuJoXXc.js (Deleted) -438 bytes 0 bytes -100.0% 🗑️
assets/app.digest-CABUkrhh.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/eye-off-CCz6atjN.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/app.miner-U3Ly6Lry.js (Deleted) -422 bytes 0 bytes -100.0% 🗑️
assets/key-round-lwmHcsSW.js (Deleted) -355 bytes 0 bytes -100.0% 🗑️
assets/bot-ByViCKS6.js (Deleted) -328 bytes 0 bytes -100.0% 🗑️
assets/trash-2-DvUFuNWr.js (Deleted) -328 bytes 0 bytes -100.0% 🗑️
assets/save-4Pm-5wqc.js (Deleted) -327 bytes 0 bytes -100.0% 🗑️
assets/git-pull-request-arrow-C3khF7CF.js (Deleted) -321 bytes 0 bytes -100.0% 🗑️
assets/list-checks-vRZvGGpt.js (Deleted) -279 bytes 0 bytes -100.0% 🗑️
assets/compass-C_M3ibyV.js (Deleted) -251 bytes 0 bytes -100.0% 🗑️
assets/history-N9nBdiNQ.js (Deleted) -237 bytes 0 bytes -100.0% 🗑️
assets/message-square-C4dBiF4j.js (Deleted) -233 bytes 0 bytes -100.0% 🗑️
assets/lock-DL6O6hZ1.js (Deleted) -206 bytes 0 bytes -100.0% 🗑️
assets/rotate-cw-C5NEjy5D.js (Deleted) -201 bytes 0 bytes -100.0% 🗑️
assets/play-D0xL8AnZ.js (Deleted) -190 bytes 0 bytes -100.0% 🗑️
assets/circle-check-DuFoVE6a.js (Deleted) -178 bytes 0 bytes -100.0% 🗑️
assets/search-HabIEJk7.js (Deleted) -174 bytes 0 bytes -100.0% 🗑️

@JSONbored
JSONbored merged commit 93daac9 into main Jul 27, 2026
7 of 8 checks passed
@JSONbored
JSONbored deleted the fix/9151-9152-9157-9164-install-permission-integrity branch July 27, 2026 07:56
@loopover-orb loopover-orb Bot added the gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. label Jul 27, 2026
@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

❌ 4 Tests Failed:

Tests completed Failed Passed Skipped
22218 4 22214 21
View the top 3 failed test(s) by shortest run time
test/integration/orb-relay.test.ts > POST /v1/orb/relay/pull > REGRESSION (#4995): a DB error inside validateOrbRelayEnrollment's own lookup ALSO returns a clean 503 broker_error, not a framework 500 (the earlier of the two DB-touching calls in this handler)
Stack Traces | 0.0127s run time
AssertionError: expected 500 to be 503 // Object.is equality

- Expected
+ Received

- 503
+ 500

 ❯ test/integration/orb-relay.test.ts:1160:24
test/integration/orb-ingest.test.ts > Orb instance registry routes (/v1/internal/orb/instances) > tolerates a list query that omits results (rows.results ?? [])
Stack Traces | 0.0305s run time
SyntaxError: Unexpected token 'I', "Internal S"... is not valid JSON
 ❯ test/integration/orb-ingest.test.ts:427:14
test/integration/orb-onboarding.test.ts > Central Orb installation registry routes (/v1/internal/orb/installations) > tolerates a list query that omits results (rows.results ?? [])
Stack Traces | 0.0414s run time
SyntaxError: Unexpected token 'I', "Internal S"... is not valid JSON
 ❯ test/integration/orb-onboarding.test.ts:57:14
test/integration/orb-relay.test.ts > POST /v1/orb/relay/register > REGRESSION (#4995): a DB error inside validateOrbRelayEnrollment's own lookup ALSO returns a clean 503 broker_error, not a framework 500 (the earlier of the two DB-touching calls in this handler)
Stack Traces | 0.135s run time
AssertionError: expected 500 to be 503 // Object.is equality

- Expected
+ Received

- 503
+ 500

 ❯ test/integration/orb-relay.test.ts:155:24

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment