Skip to content

feat(slack): support shared GitHub Cloud Agent workflows - #6089

Merged
pandemicsyn merged 125 commits into
mainfrom
feat/github-connections-pr3
Sep 16, 2026
Merged

pandemicsyn merged 125 commits into
mainfrom
feat/github-connections-pr3

Conversation

@pandemicsyn

@pandemicsyn pandemicsyn commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • allow an already-connected Slack integration to remain compatible when its Kilo owner enters shared-GitHub mode
  • aggregate cached repositories from every healthy tenant-owned GitHub association with exact association provenance
  • revalidate the selected association for the Slack owner and forward its exact githubIntegrationId to Cloud Agent

Scope

  • Slack must already be connected before sharing
  • PR2 provider OAuth attempts continue to fence Slack setup against sharing
  • no Slack OAuth, credential, Chat SDK, webhook, uninstall, reconnect, Enterprise, schema, or migration changes
  • no synthetic all-access repository fallback

Validation

  • 22 focused tests, repeated twice after final fixture cleanup
  • healthy/unhealthy sibling inventory and exact tenant provenance
  • active Slack compatibility plus Linear/Discord and live-attempt fences
  • web typecheck and lint
  • formatting and diff checks
  • exact final inventory: 3 production files, 3 test files, no migration

…liation

Split 0244 into three independently-tracked migrations so a failed or
interrupted deploy can never strand durably-committed DDL behind an
untracked migration, and never leaves a window with neither the old
nor the new uniqueness protection in place:

- 0244_salty_cyclops.sql: plain transactional additions only (new
  tables, new columns, all FK constraints including the validating
  platform_integrations FK). No explicit COMMIT/BEGIN, so Postgres
  rolls the whole file back atomically on any failure; a retry is
  always clean.
- 0245_calm_lockheed.sql: only the four CREATE UNIQUE INDEX
  CONCURRENTLY replacement indexes, fenced with the repo's approved
  COMMIT/BEGIN pattern. Does NOT drop the old two unique indexes.
- 0246_clean_the_renegades.sql: drops the two old unique indexes and
  adds the sharing_mode CHECK constraint, only after 0245 has been
  independently tracked as applied, so the replacements are always
  confirmed built before the originals are removed.

Verified empirically: applying only through 0245 leaves both the old
and new indexes coexisting (all valid) - there is no point in the
sequence where the uniqueness invariant is unprotected. 0246 then
applies cleanly as its own later migration.

Per packages/db/AGENTS.md, deliberately did not add IF NOT EXISTS or
a pre-emptive DROP INDEX IF EXISTS around the CONCURRENTLY builds:
the skill explicitly prohibits automatically masking a failed/invalid
concurrent index build that way and requires explicit operator
inspection and recovery (DROP INDEX CONCURRENTLY / REINDEX INDEX
CONCURRENTLY) instead. The file split already achieves retry-safety
at the migration-tracking level without needing that.

Also hardens 0243: replace the silent UPDATE that nulled any
dangling platform_integrations.github_installation_id with a DO 12662
guard that RAISEs if any orphan exists. Nulling was a behavior change
in disguise - for a row with github_app_type IS NULL, clearing the
reference makes it newly eligible for the legacy bot-link fallback
lookup (which treats github_app_type = 'standard' OR IS NULL as
equivalent). Since production orphan_count is confirmed 0, assert the
invariant instead of silently mutating data/eligibility.

Verified: drizzle-kit check passes; the full chain (including a
staged partial-apply through 0245 only, then 0246 separately) applies
cleanly from scratch against a fresh local Postgres; schema.ts is
unchanged (same end-state schema, just staged across more granular
migrations); packages/db/src/schema.test.ts (103 tests) passes.
Each of the four CREATE UNIQUE INDEX CONCURRENTLY statements in
0245_calm_lockheed.sql had no idempotency guard. CONCURRENTLY can
leave an INVALID index under its name if a build fails partway or is
interrupted; a blind retry of CREATE UNIQUE INDEX CONCURRENTLY on the
same name then fails with 'already exists' instead of rebuilding it -
the exact non-retryable strand Finding #1 addressed, just moved down
to the index-build step.

Precede each build with DROP INDEX CONCURRENTLY IF EXISTS on its
exact name: a no-op on the happy path (index doesn't exist yet), and
a clean removal of a leftover invalid index otherwise, so every retry
converges to the same end state. Per packages/db/AGENTS.md, deliberately
not using CREATE ... CONCURRENTLY IF NOT EXISTS, since that would leave
a leftover invalid index in place unrebuilt instead of recovering it;
DROP INDEX CONCURRENTLY is the exact recovery mechanism the skill
names, just applied proactively rather than only after manual
diagnosis. Both DROP CONCURRENTLY and CREATE CONCURRENTLY remain in
the existing post-COMMIT/pre-BEGIN section, outside any transaction
block, as required. No change to the final index set, names, or
schema.ts/snapshots.

Verified: drizzle-kit check passes. Full chain applies cleanly from
scratch. Directly simulated the failure mode: seeded a duplicate-data
conflict, forced one of the four builds to fail and leave an INVALID
index (confirmed indisvalid=false), confirmed a blind retry without
this guard fails with 'already exists' (reproducing the bug), then
repaired the data and confirmed the guarded migration now retries
cleanly to a fully valid final index set. packages/db/src/schema.test.ts
(103 tests) passes.
Reconciles main's #6178/#6179 refactor of
apps/web/src/lib/integrations/github/runtime-authorization.ts (reason-enum
diagnostics) with this branch's canonical-installation health and
sharing_mode=exclusive gating, into one coherent file:

Kept from main:
- GitHubRuntimeAuthorizationRejectionReason union and
  getGitHubRuntimeAssociationRejectionReason(association) returning a
  specific reason or null.
- GitHubRuntimeAuthorizationError(reason, diagnostics) with
  { installationId, appType, integrationIds }.
- isGitHubRuntimeAssociationAuthorized as a boolean wrapper
  (=== null).
- The new caller in prepare-review-payload.ts (threaded the exact
  platform_integrations id it already has in scope as
  expectedIntegrationId, since generateGitHubInstallationToken already
  supported that third argument).

Kept from this branch (extended the reason function to cover it,
rather than duplicating it separately):
- Two new reasons, installation_unavailable and sharing_not_allowed,
  covering canonical installation lifecycle_state/suspended_at/
  deleted_at/auth_invalid_at and sharing_mode.
- assertGitHubInstallationRuntimeAuthorized's third argument
  (expectedIntegrationId?: string), the notExists-canonical guard for
  unbound associations, and .limit(expectedIntegrationId ? 1 : 2).
- All of the original SQL candidacy+health filtering (association
  integration_status/disconnected_at/suspended_at/auth_invalid_at, and
  the canonical join's lifecycle_state/sharing_mode/suspended_at/
  deleted_at/auth_invalid_at). Initially tried moving these into the
  reason function only (matching main's diagnostics-first philosophy),
  but apps/web/src/lib/integrations/db/github-installations.test.ts's
  'keeps local disconnect separate from shared upstream suspension
  recovery' integration test caught a real regression: without the
  SQL-level filters, a disconnected former owner's stale row and a
  still-healthy owner's row both come back and get misclassified as
  ambiguous_association instead of resolving to the healthy one. This
  is a genuine authorization-outcome change, not just a diagnostics
  change, so the original filters stay in SQL; the reason function's
  checks remain as a correctness backstop for callers (like
  prepare-review-payload) that fetch an association through a
  different, unfiltered path.

Explicitly out of scope, left for PR4: expectedIntegrationId/allowShared
normalization and the sharing_mode = 'web_cloud_agent' exact-id carve-out
described for 'our stack' do not actually exist yet on this branch
(verified: they only exist in feat/github-connections-pr4's own commits
15ae070 and f53e068). Per instructions not to touch PR3/PR4, this
merge does not add them; PR4 will layer that carve-out on top of this
file when it rebases. The empty-string-expectedIntegrationId-behaves-
as-generic invariant already holds today, unprompted, since the
existing ternaries () treat an empty
string as falsy.

Test reconciliation: the auto-merged test file combined both sides'
association fixtures/assertions without a textual conflict, but still
had one main-only case ('ignores canonical %s lifecycle and health
markers') asserting the old shadow-state behavior this merge replaces;
rewrote it to assert rejection. Added rejectionCases coverage for the
two new reasons, and two focused tests for the exact-id vs
empty-string-id call shapes. Fixed one main-only DB-mock test's
db.select call-count expectation (1 -> 2) to account for the
notExists subquery.

Verified: pnpm --filter web typecheck clean; oxlint clean on all
touched files; 174 tests pass across the full affected auth/webhook
suite (runtime-authorization, webhook-handler +
webhook-handler-dispatch, installation-handler, connection-service,
github-integration-helpers, prepare-review-payload,
github-installations), plus platform-integrations,
github-installation-uninstall, bot/platforms/github,
github-link-state, and both cloud-agent-next-router suites (320
tests) for related call sites. No 'review-shared-repositories' test
file exists in the repo under that name; ran the closest matching
suites instead.
…e main merge

The main #6178/#6179 reconciliation merge (81628ee) kept sharing_mode
gating unconditionally exclusive-only, which regressed a shared
(web_cloud_agent) association's ability to ever authorize, even with an
exact association id pinned. Note: this carve-out never actually
existed on feat/github-connections-pr2 before that merge (verified via
git history: it only exists on feat/github-connections-pr4's commits
15ae070 and f53e068) — but it is a real, needed capability for
shared installations to read/refresh their own repository inventory,
so re-applying it here now, adapted to main's reason-returning shape:

1. getGitHubRuntimeAssociationRejectionReason(association, options?)
   takes { allowShared?: boolean }; sharing_mode is only permitted to
   be 'web_cloud_agent' when allowShared is explicitly true, otherwise
   it must be 'exclusive'. isGitHubRuntimeAssociationAuthorized forwards
   the same options (defaulting to exclusive-only).
2. assertGitHubInstallationRuntimeAuthorized normalizes
   expectedIntegrationId once into exactAssociationId (empty string ->
   undefined) and derives allowShared = exactAssociationId !== undefined,
   so the id predicate, the row limit, and the sharing-mode carve-out
   can never diverge (the F3 invariant).
3. The SQL's canonical-installation OR-branch permits sharing_mode IN
   (exclusive, web_cloud_agent) only when allowShared; otherwise
   exclusive only.
4. The post-query reason check passes { allowShared } through so it
   agrees with what the SQL already filtered for.

Every other check (ownership, local health, canonical installation
health, ambiguous_association for the generic path) is unchanged.

Ported the PR4 test coverage for this onto PR2's reason-based API:
- runtime-authorization.test.ts: allowShared true/false/unset on the
  pure reason function and boolean wrapper, plus mock-level exact-id
  vs generic-path authorization of a shared association.
- github-installations.test.ts: a real end-to-end integration test
  (two orgs sharing one installation) covering exact-id shared
  authorization, generic-path rejection, an added empty-string-id F3
  case, unknown-id rejection, and local/owner health enforcement on
  the exact-id path; plus canonical-installation-unhealthy rejection.
- New review-shared-repositories.test.ts (ported from PR4): an
  end-to-end adapter-level regression proving fetchGitHubRepositories
  succeeds for an exact healthy shared association.

Did not port PR4's separate connection-service.ts repository-caching-
at-attach fix (15ae070) — that is an unrelated bug in a different
function, out of scope for this regression fix.

Verified: pnpm --filter web typecheck clean; oxlint clean on all 4
changed files; pnpm format (line-wrap only); git diff --check clean;
no schema.ts or migration changes. 182 tests pass across the full
required suite (runtime-authorization, review-shared-repositories,
webhook-handler + webhook-handler-dispatch.integration,
installation-handler, connection-service, github-integration-helpers,
prepare-review-payload, github-installations).
Resolves a migration-number collision: main added 0243_ambiguous_namor
and 0244_many_vermin (organization_service_fee_exemptions,
stripe_service_fee_assessments) at the same indices our stack already
used. Kept main's 0243/0244 as-is and renumbered our five migrations
above them via the drizzle workflow (schema.ts staged through the same
sequence used to originally generate them, then pnpm drizzle generate
at each step, never hand-writing journal/snapshot content):

  0243_ambiguous_namor            (main, unchanged)
  0244_many_vermin                (main, unchanged)
  0245_reconcile_orphaned_github_installation_id  (was 0243)
  0246_salty_cyclops               (was 0244)
  0247_calm_lockheed               (was 0245)
  0248_clean_the_renegades         (was 0246)
  0249_groovy_agent_zero           (new)

Verified every regenerated file is byte-identical to its pre-renumber
content (0245/0246/0247/0248 diffed clean against backups; 0247 still
carries the DROP INDEX CONCURRENTLY IF EXISTS retry-safety guards and
COMMIT/BEGIN fencing unchanged). 0249 is a new migration: pulled the
schema-only part of feat/github-connections-pr4's commit 805e161
(github_installation_webhook_receipts.status + its CHECK constraint)
forward onto this branch, since PR4 depends on it and the task asked
for our five migrations to land at 0245-0249 inclusive of it. Did not
pull PR4's accompanying application-logic changes (github-installations.ts,
webhook-handler.ts, routers) - that is a separate feature port, out of
scope for a migration renumber.

schema.ts, schema.test.ts, and all four cloud-agent-next router files
(apps/web/src/routers/cloud-agent-next-router.ts + .test.ts,
organizations/organization-cloud-agent-next-router.ts + .test.ts)
auto-merged cleanly with no conflicts; verified by inspection that both
sides' changes are present (main's service-fee tables, worktree-access
control/assertSessionWorktree, getMessageResult; our sharing_mode/
sharing_admission_checked_at columns, the four canonical/pending unique
indexes, provider_oauth_attempts, the platform_integrations FK, and the
githubAppType/platformIntegrationId router output fields).

Validated per the database-migrations skill: drizzle-kit check is
clean; drizzle-kit generate reports "No schema changes, nothing to
migrate" (schema.ts matches the regenerated chain exactly); the full
chain 0000-0249 replays successfully via pnpm drizzle:verify-bootstrap
against a fresh throwaway database, and again via a direct migrate
run, confirming all four unique index names, the
platform_integrations_github_installation_id_..._fk (ON DELETE
restrict), and both CHECK constraints
(github_app_installations_sharing_mode_check,
github_installation_webhook_receipts_status_check) are present with
unchanged names. _journal.json has 250 entries, idx 0-249 with no gaps
or duplicates.

pnpm --filter web typecheck clean; oxlint clean on schema.ts; 587 tests
pass across github-installations, connection-service,
runtime-authorization, webhook-handler (+ dispatch integration),
installation-handler, platform-integrations, both cloud-agent-next
routers, schema.test.ts, review-shared-repositories, prepare-review-payload,
and github-integration-helpers. pnpm format made no changes; git diff
--check clean (excluding pre-existing trailing whitespace in a vendored
patches/react-native@0.86.3.patch, unrelated to this merge). No conflict
markers anywhere in the repository.
Base automatically changed from feat/github-connections-pr2 to main September 16, 2026 19:59
@pandemicsyn
pandemicsyn merged commit 606d567 into main Sep 16, 2026
25 checks passed
@pandemicsyn
pandemicsyn deleted the feat/github-connections-pr3 branch September 16, 2026 20:38
pandemicsyn added a commit that referenced this pull request Sep 16, 2026
…ns-pr4

Reconciles PR4's security hardening with the squashed PR2 (#5978) + PR3
(#6089) now in main. Eleven files conflicted; every resolution keeps main's
shared-installation base and layers PR4's hardening on top (verified PR4 had
no behavior that main lacked in these regions — main's conflicted hunks were
the pre-hardening base):

- callback/route.ts: the atomic legacy write+guard+bind transaction under
  lockGitHubInstallationIdentity, the NULL-safe
  'col::text IS DISTINCT FROM owner.id' competing-owner guard, the
  verifiedIdentity gating (verified path never hits the legacy guard), and
  the installation_already_claimed mapping.
- installation-handler.ts: the unbound-peer locked existence check
  (pending|active, bound or unbound, excluding the pending row and
  disconnected rows), the single-transaction complete+bind, and connected
  association resolution for suspend/unsuspend.
- webhook-handler.ts: isRoutableGitHubIntegration health gating, connected
  tenant resolution for suspend/unsuspend, and exact association id
  threading into the deferred-dispatch authorization check.
- github-installations.ts: the incumbent personal-owner reconnect guard, the
  active/disconnected sharing split, review terminalization with
  settle-after-commit, identity-based canonical/sibling resolution on
  uninstall, and the transaction-capable bind.
- platform-integrations.ts: findConnectedIntegrationByInstallationId and the
  transaction-capable writers; connection-service keeps the best-effort
  all-repositories inventory sync.
- github-apps-router.ts: disconnected rows excluded from installation
  cardinality; sharing-compatibility.ts: disconnected associations excluded
  from compatibility assessment.
- runtime-authorization.ts needed no conflict resolution: main's
  reason-based API already carries the exact-id allowShared carve-out, so the
  generic/empty-string path stays exclusive-only.

Test files are a union of both sides (all main tests retained, no duplicate
test names); main's obsolete 'reconnects the same association after local
disconnect and rejects another owner' is not kept because it asserts the
superseded claimed_by_other_owner behavior for a disconnected incumbent,
whose two scenarios PR4 covers with its split tests.

Migrations are untouched: packages/db/src/migrations and schema.ts are
byte-identical to main (single 0243_ambiguous_namor..0249_groovy_agent_zero).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants