Skip to content

fix: installation-scoped tenant isolation for hosted multi-tenant app - #1098

Closed
Helios531 wants to merge 6 commits into
JSONbored:mainfrom
Helios531:feat/multi-tenant-productization
Closed

fix: installation-scoped tenant isolation for hosted multi-tenant app#1098
Helios531 wants to merge 6 commits into
JSONbored:mainfrom
Helios531:feat/multi-tenant-productization

Conversation

@Helios531

Copy link
Copy Markdown
Contributor

Summary

This change fixes multi-tenant isolation for hosted app usage.
Before this fix, some tenant-sensitive state could be resolved too broadly:

  • repository settings were effectively repo-scoped
  • BYOK AI keys were effectively repo-scoped
  • private runtime guardrail config could fall back to a shared repo slug key

This patch makes those paths installation-scoped so hosted tenants are isolated from each other, while preserving a safe legacy/self-host fallback path.

Related Issues

Change Type

  • Bug fix
  • Security fix
  • Multi-tenant isolation fix
  • Database migration
  • Test update
  • New feature
  • Docs-only change

What Changed

  • Added installation-scoped storage for repository_settings
  • Added installation-scoped storage for repository_ai_keys
  • Added DB migration for legacy data
  • Updated settings resolution to use active installation scope
  • Updated BYOK key resolution to use active installation scope
  • Updated private guardrail config lookup to prefer installation-qualified keys
  • Added regression tests for settings isolation
  • Added regression tests for BYOK isolation
  • Added regression tests for guardrail-config precedence

Real Behavior Proof

Before

  • Different hosted tenants using the same repo slug could risk reading shared private runtime config
  • Settings and BYOK records were not fully isolated per installation

After

  • Settings resolve by repo_full_name + installation_id
  • BYOK keys resolve by repo_full_name + installation_id
  • Guardrail config lookup prefers:
    1. installation:{installationId}:{owner/repo}
    2. installation:{installationId}:{slug}
    3. owner/repo
    4. slug

This prevents hosted tenant cross-bleed for private runtime config and secrets.

Checklist

  • Root cause identified
  • Installation-scoped isolation implemented
  • Legacy compatibility preserved
  • Migration added
  • Regression tests added
  • Validation completed

@Helios531
Helios531 requested a review from JSONbored as a code owner June 23, 2026 19:33
@dosubot dosubot Bot added the size:XL label Jun 23, 2026
@superagent-security

Copy link
Copy Markdown
Contributor

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

@Helios531

Copy link
Copy Markdown
Contributor Author

@JSONbored Could you review my PR?

@ghost

ghost commented Jun 23, 2026

Copy link
Copy Markdown

Caution

🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥

🛑 Gittensory review — closed

14 files · 2 AI reviewers · 1 blocker · readiness 93/100 · CI pending

🛑 Closed — AI reviewers agree on a likely critical defect: src/db/repositories.ts: markInstallationDeleted and markRepositoriesRemovedFromInstallation set installationId: sql`null` even though the column is defined as NOT NULL with a default of 0, causing a constraint violation. — Resolve the flagged defect, or override if the AI reviewers are mistaken, then re-run the gate.

Review summary
The PR adds installation‑scoped isolation for repository settings, BYOK keys, and guardrail config, introduces a migration, updates DB schema, and adds subnet claim validation via metagraphed. Core logic correctly resolves the active installation ID and scopes queries, and new tests cover the isolation behavior. However, the deletion helpers now attempt to write NULL into a NOT NULL column, which will cause runtime DB errors.

Blockers

  • src/db/repositories.ts: markInstallationDeleted and markRepositoriesRemovedFromInstallation set installationId: sql`null` even though the column is defined as NOT NULL with a default of 0, causing a constraint violation.
  • src/db/repositories.ts: markRepositoriesRemovedFromInstallation suffers the same NULL assignment to a NOT NULL installation_id column.
  • The `resolveScopedInstallationId` function in `src/db/repositories.ts` is not defined in the diff, but it is used in several places. Verify that it is defined elsewhere in the file or imported correctly.

<details>
<summary>Nits (5)</summary>

  • loadHardGuardrailGlobs test expects two KV gets, but the function returns after the first successful key, so the second call never happens.
  • resolveScopedInstallationId is defined after its first use, relying on function hoisting; while valid, placing it earlier would improve readability.
  • Redundant null‑coalescing when inserting installationId (e.g., resolved.installationId ?? 0) after it has already been resolved.
  • repositorySettings and repositoryAiKeys tables lost explicit primary keys; they now rely solely on unique indexes, which may affect some ORM expectations.
  • In getRepositorySettings the variable effectiveRow is unnecessary; the original row could be used directly.
    </details>

Why this is blocked

  • AI reviewers agree on a likely critical defect: src/db/repositories.ts: markInstallationDeleted and markRepositoriesRemovedFromInstallation set installationId: sql`null` even though the column is defined as NOT NULL with a default of 0, causing a constraint violation.: src/db/repositories.ts: markInstallationDeleted and markRepositoriesRemovedFromInstallation set installationId: sql`null` even though the column is defined as NOT NULL with a default of 0, causing a constraint violation.
Signal Result Evidence
Code review ❌ 1 blocker 2 reviewers, synthesized
Linked issue ⚠️ Missing No linked issue or no-issue rationale found.
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Review load ✅ 20/20 Readiness component derived from cached public PR metadata and labels; size label size:XL.
Validation evidence ✅ 25/25 PR body includes validation/test evidence.
Open PR queue ✅ 10/10 0 open PR(s), 0 likely reviewable.
Contributor context ✅ Confirmed Gittensor contributor Helios531; Gittensor profile; 56 PR(s), 2 issue(s).
Gate result ❌ Blocking Repo-configured hard blocker found.
Nits — 1 non-blocking
  • No linked issue detected — If this PR is intended to solve an issue, link it explicitly in the PR body.
Review context
  • Author: Helios531
  • Role context: outside_contributor
  • Public audience mode: oss maintainer
  • Lane context: Repository is configured for direct PR review.
  • Public profile languages: Python, TypeScript, MDX, Clojure, Dart, JavaScript, Kotlin, PHP
  • Official Gittensor activity: 56 PR(s), 2 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Explain no-issue PR.
  • Link the issue being solved, or explicitly explain why this is a no-issue PR.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Review load = cached public PR metadata such as size labels, changed paths, and preflight status.
  • Open PR queue = repo-wide review pressure; it is not a PR quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.
Review details

Generated from public PR metadata and the diff. Advisory only; deterministic signals remain authoritative.

The PR adds installation‑scoped isolation for repository settings, BYOK keys, and guardrail config, introduces a migration, updates DB schema, and adds subnet claim validation via metagraphed. Core logic correctly resolves the active installation ID and scopes queries, and new tests cover the isolation behavior. However, the deletion helpers now attempt to write NULL into a NOT NULL column, which will cause runtime DB errors.

Blockers

  • src/db/repositories.ts: markInstallationDeleted and markRepositoriesRemovedFromInstallation set installationId: sql`null` even though the column is defined as NOT NULL with a default of 0, causing a constraint violation.
  • src/db/repositories.ts: markRepositoriesRemovedFromInstallation suffers the same NULL assignment to a NOT NULL installation_id column.
  • The `resolveScopedInstallationId` function in `src/db/repositories.ts` is not defined in the diff, but it is used in several places. Verify that it is defined elsewhere in the file or imported correctly.

<details>
<summary>Nits (5)</summary>

  • loadHardGuardrailGlobs test expects two KV gets, but the function returns after the first successful key, so the second call never happens.
  • resolveScopedInstallationId is defined after its first use, relying on function hoisting; while valid, placing it earlier would improve readability.
  • Redundant null‑coalescing when inserting installationId (e.g., resolved.installationId ?? 0) after it has already been resolved.
  • repositorySettings and repositoryAiKeys tables lost explicit primary keys; they now rely solely on unique indexes, which may affect some ORM expectations.
  • In getRepositorySettings the variable effectiveRow is unnecessary; the original row could be used directly.
    </details>

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed


💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →.

Checked by Gittensory, a quiet PR intelligence layer for OSS maintainers.

  • Re-run Gittensory review

@ghost ghost added gittensory:reviewed gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. labels Jun 23, 2026
@codecov

codecov Bot commented Jun 23, 2026

Copy link
Copy Markdown

❌ 14 Tests Failed:

Tests completed Failed Passed Skipped
1881 14 1867 0
View the top 3 failed test(s) by shortest run time
test/unit/queue.test.ts > queue processors > audits advisory context check publish failures without blocking webhook processing
Stack Traces | 0.0769s run time
AssertionError: expected undefined to match object { Object (event_type) }

- Expected:
{
  "event_type": "github_app.pr_check_run_publish_failed",
}

+ Received:
undefined

 ❯ test/unit/queue.test.ts:3244:27
test/unit/queue.test.ts > queue processors > audits advisory context check permission failures without blocking webhook processing
Stack Traces | 0.0784s run time
AssertionError: expected undefined to match object { …(4) }

- Expected:
{
  "actor": "contributor",
  "event_type": "github_app.check_run_permission_missing",
  "outcome": "error",
  "target_key": "JSONbored/gittensory#24",
}

+ Received:
undefined

 ❯ test/unit/queue.test.ts:3190:19
test/unit/queue.test.ts > queue processors > audits disabled public-surface skips without miner lookup
Stack Traces | 0.0802s run time
AssertionError: expected { fetch: 1, repoWideReads: +0 } to deeply equal { fetch: +0, repoWideReads: +0 }

- Expected
+ Received

  {
-   "fetch": 0,
+   "fetch": 1,
    "repoWideReads": 0,
  }

 ❯ test/unit/queue.test.ts:3291:19
test/unit/queue.test.ts > queue processors > fails closed when official miner detection is unavailable
Stack Traces | 0.102s run time
AssertionError: expected [ { …(3) }, { …(3) } ] to deeply equal ArrayContaining{…}

- Expected
+ Received

- ArrayContaining [
-   ObjectContaining {
+ [
+   {
      "detail": "miss",
      "event_type": "github_app.miner_detection_cache_miss",
      "outcome": "completed",
    },
-   ObjectContaining {
-     "detail": StringContaining "Gittensor API failed",
+   {
+     "detail": "Gittensor API failed for https://api.gittensor.io/miners (503)",
      "event_type": "github_app.miner_detection_unavailable",
      "outcome": "error",
-   },
-   ObjectContaining {
-     "detail": "miner_detection_unavailable",
-     "event_type": "github_app.pr_visibility_skipped",
-     "outcome": "completed",
    },
  ]

 ❯ test/unit/queue.test.ts:3814:27
test/unit/queue.test.ts > queue processors > keeps GitHub-history-only contributors quiet through not_found cache hits and expiry
Stack Traces | 0.135s run time
AssertionError: expected [ { …(2) }, { …(2) }, { …(2) } ] to deeply equal ArrayContaining{…}

- Expected
+ Received

- ArrayContaining [
-   ObjectContaining {
+ [
+   {
      "detail": "miss",
      "event_type": "github_app.miner_detection_cache_miss",
    },
-   ObjectContaining {
+   {
      "detail": "not_found",
      "event_type": "github_app.miner_detection_cache_hit",
    },
-   ObjectContaining {
-     "detail": "not_official_gittensor_miner",
-     "event_type": "github_app.pr_visibility_skipped",
+   {
+     "detail": "miss",
+     "event_type": "github_app.miner_detection_cache_miss",
    },
  ]

 ❯ test/unit/queue.test.ts:3685:27
test/unit/queue.test.ts > queue processors > skips bots and maintainer authors, and keeps explicitly enabled checks minimal
Stack Traces | 0.137s run time
AssertionError: expected { minerList: 1, checks: +0 } to deeply equal { minerList: 1, checks: 1 }

- Expected
+ Received

  {
-   "checks": 1,
+   "checks": 0,
    "minerList": 1,
  }

 ❯ test/unit/queue.test.ts:3138:19
test/unit/queue.test.ts > queue processors > applies repo command authorization policy overrides during issue_comment handling
Stack Traces | 0.14s run time
AssertionError: expected { commentsCreated: +0, token: 1, …(1) } to deeply equal { commentsCreated: 1, token: 1, …(1) }

- Expected
+ Received

  {
-   "commentsCreated": 1,
-   "minerList": 0,
+   "commentsCreated": 0,
+   "minerList": 1,
    "token": 1,
  }

 ❯ test/unit/queue.test.ts:4332:19
test/unit/queue.test.ts > queue processors > records an aggregate public-surface failure when no configured output publishes
Stack Traces | 0.142s run time
AssertionError: expected { detail: 'comment,label', …(1) } to match object { detail: 'comment' }
(1 matching property omitted from actual)

- Expected
+ Received

  {
-   "detail": "comment",
+   "detail": "comment,label",
  }

 ❯ test/unit/queue.test.ts:3427:23
test/unit/queue.test.ts > queue processors > recovers confirmed miners after the unavailable cache window expires
Stack Traces | 0.152s run time
AssertionError: expected { minerList: 2, labels: 4 } to deeply equal { minerList: 2, labels: 3 }

- Expected
+ Received

  {
-   "labels": 3,
+   "labels": 4,
    "minerList": 2,
  }

 ❯ test/unit/queue.test.ts:3900:19
test/unit/queue.test.ts > queue processors > records label-only public-surface failures without creating duplicate comments
Stack Traces | 0.182s run time
AssertionError: expected { comments: 2, labels: 2 } to deeply equal { comments: +0, labels: 2 }

- Expected
+ Received

  {
-   "comments": 0,
+   "comments": 2,
    "labels": 2,
  }

 ❯ test/unit/queue.test.ts:3606:19
test/unit/queue.test.ts > queue processors > uses cached confirmed miner detection for label-only public surfaces
Stack Traces | 0.195s run time
AssertionError: expected { Object (comments, labels, ...) } to deeply equal { comments: +0, labels: 6, …(1) }

- Expected
+ Received

  {
-   "comments": 0,
-   "labels": 6,
+   "comments": 4,
+   "labels": 8,
    "minerList": 1,
  }

 ❯ test/unit/queue.test.ts:3533:19
test/unit/queue.test.ts > queue processors > renders the unified PR-review comment when the flag is on and the gate evaluates
Stack Traces | 0.203s run time
AssertionError: expected '<!-- gittensory-pr-panel:v1 -->\n\n> …' to contain '**Code review**'

- Expected
+ Received

- **Code review**
+ <!-- gittensory-pr-panel:v1 -->
+
+ > [!TIP]
+ > ## Gittensory PR readiness looks good
+ > Public GitHub metadata was checked for review readiness. Gittensor-specific context appears only when confirmed.
+ >
+ > **Readiness score: 100/100**
+ >
+ > | Signal | Result | Evidence | Action |
+ > | --- | --- | --- | --- |
+ > | Linked issue | ✅ Linked | #1 | No action. |
+ > | Related work | ✅ No active overlap found | No same-issue or scoped active PR overlap found. | No action. |
+ > | Review load | ✅ 20/20 | Readiness component derived from cached public PR metadata and labels. | No action. |
+ > | Validation evidence | ✅ 25/25 | PR body includes validation/test evidence. | No action. |
+ > | Open PR queue | ✅ 10/10 | 1 open PR(s), 1 likely reviewable. | No action. |
+ > | Contributor context | ✅ Confirmed Gittensor contributor | [oktofeesh1](https://github.com/oktofeesh1); [Gittensor profile](https://gittensor.io/miners/details?githubId=123); 4 PR(s), 0 issue(s). | No action. |
+ > | Gate result | ⚠️ Advisory only | Advisory only. | No action. |
+
+ <details>
+ <summary>Signal definitions</summary>
+
+ - Related work = same linked issue, overlapping active PRs, or title/path similarity.
+ - Review load = cached public PR metadata such as size labels, changed paths, and preflight status.
+ - Open PR queue = repo-wide review pressure; it is not a PR quality failure.
+ - Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.
+
+ </details>
+
+ <details>
+ <summary>Review context</summary>
+
+ - Author: `oktofeesh1`
+ - Role context: outside_contributor
+ - Public audience mode: oss maintainer
+ - Lane context: Repository is configured for direct PR review.
+ - Public profile languages: TypeScript
+ - Official Gittensor activity: 4 PR(s), 0 issue(s).
+ - PR-specific overlap: none found.
+
+ </details>
+
+ <details>
+ <summary>Maintainer notes</summary>
+
+ - No public-safe advisory findings were generated from cached metadata.
+
+ </details>
+
+ <details>
+ <summary>Contributor next steps</summary>
+
+ - Keep the PR focused and include validation evidence before maintainer review.
+
+ </details>
+
+ - [ ] <!-- gittensory-rerun-review:v1 --> Re-run Gittensory review
+
+ ---
+ 💰 **Earn for open-source contributions like this.** [Gittensor](https://gittensor.io) lets GitHub contributors earn for the work they already do — [register to start earning →](https://gittensor.io/miners/repository?name=JSONbored%2Fgittensory&tab=miners).
+
+ Checked by [Gittensory](https://gittensory.aethereal.dev), a quiet PR intelligence layer for OSS maintainers.

 ❯ test/unit/queue.test.ts:2901:24
test/unit/queue.test.ts > queue processors > records public comment failure without blocking the context check
Stack Traces | 0.213s run time
AssertionError: expected +0 to be 1 // Object.is equality

- Expected
+ Received

- 1
+ 0

 ❯ test/unit/queue.test.ts:3362:26
test/unit/queue.test.ts > queue processors > inline-fetches the PR files and renders failing CI check names + reasons in the unified comment (FIX B + D3)
Stack Traces | 0.217s run time
AssertionError: expected 0 to be greater than 0
 ❯ test/unit/queue.test.ts:3051:26

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

JSONbored added a commit that referenced this pull request Jun 23, 2026
…(guard blocks merge, not rejection) (#1106)

#1090 gated willClose on !guardrailHit, so a rejected CONTRIBUTOR PR touching a guarded path was HELD instead
of closed (e.g. gittensory #1098: rejected, touches src/review|services|signals → stayed open with a
'closing' message). That contradicts the spec: 'guarded + would-merge → hold; otherwise → closure.'

The hard-guardrail exists to stop auto-MERGING/APPROVING crucial-path changes without owner review (canMerge
+ approve still gate on !guardrailHit). It must NOT keep a rejected PR open — closing rejects bad changes and
merges nothing, so it is always safe. willClose no longer checks guardrailHit; owner/automation PRs are still
never closed (isContributor gates it); GOOD-but-guarded PRs still fall through to the owner (held).

Test flipped: a failing contributor PR on a guarded path now CLOSES.

@JSONbored JSONbored left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue is already assigned to maintainer-only and is not worth any gittensor points. Closing.

@JSONbored

Copy link
Copy Markdown
Owner

T

@JSONbored JSONbored closed this Jun 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Multi-tenant productization (hosted App = primary)

2 participants