mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-02 08:28:48 +01:00
fix(github): serialize binding delivery with company admission
Bound binding calls and wait for each in-flight batch before releasing lifecycle locks. Keep archived companies from dispatching saved binding changes. Verify company isolation and the startup history reconciliation contract. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
a4dbaff604
commit
2fb56dba9e
2 files changed
+30
-11
No files matched your search
@@ -99,9 +99,10 @@ describeDatabase("GitHub coordinator with PostgreSQL and controlled Cloud HTTP (
|
||||
const ew = registerGitHubConnectionEventWork(db, coordinator, events, () => true);
|
||||
const cw = registerGitHubConnectionContinuityWork(db, coordinator, f.service, () => true);
|
||||
try {
|
||||
await Promise.all([ew.ready, cw.ready]); expect(leases).toBe(0);
|
||||
await Promise.all([ew.ready, cw.ready]); expect(leases).toBe(1);
|
||||
const startupLeases = leases;
|
||||
await f.service.refreshManagedGitHubGrantAccess(f.connection, f.grant, { actorType: "system", actorId: "test" });
|
||||
await vi.waitFor(() => expect(leases).toBeGreaterThan(0));
|
||||
await vi.waitFor(() => expect(leases).toBeGreaterThan(startupLeases));
|
||||
await vi.advanceTimersByTimeAsync(31_000); await settled(coordinator);
|
||||
const [saved] = await db.select().from(connectionGrants).where(eq(connectionGrants.id, f.grant.id));
|
||||
expect(saved!.providerTenant!.github!.installationIds).toEqual(["101"]);
|
||||
@@ -142,6 +143,16 @@ describeDatabase("GitHub coordinator with PostgreSQL and controlled Cloud HTTP (
|
||||
expect(await restarted.nextGitHubConnectionContinuityAt()).toBeNull();
|
||||
});
|
||||
|
||||
it("settles only the company and grant that own saved binding debt", async () => {
|
||||
const f = await fixture(["101"]), other = await fixture(["202"]);
|
||||
await db.update(connectionGrants).set({ providerTenant: { ...f.grant.providerTenant, github: { ...f.grant.providerTenant!.github!,
|
||||
bindingSync: { id: randomUUID(), removeInstallationIds: [] } } } }).where(eq(connectionGrants.id, f.grant.id));
|
||||
await f.service.sweepGitHubConnectionContinuity();
|
||||
expect(bindings).toHaveLength(1); expect(bindings[0]).toMatchObject({ grantId: f.grant.id, installationId: "101" });
|
||||
const [untouched] = await db.select().from(connectionGrants).where(eq(connectionGrants.id, other.grant.id));
|
||||
expect(untouched!.providerTenant).toEqual(other.grant.providerTenant);
|
||||
});
|
||||
|
||||
it("retains a real deadline when successful renewal leaves a short-lived credential unchanged", async () => {
|
||||
const f = await fixture(["101"]); fakeClock();
|
||||
vi.spyOn(globalThis, "fetch").mockImplementation((url, init) => String(url).startsWith("https://api.github.com")
|
||||
|
||||
@@ -6,7 +6,7 @@ import { measureRunPhase } from "../execution-preparation-timing.js";
|
||||
import { measureRunTransaction } from "../execution-transaction-timing.js";
|
||||
import { and, eq, sql } from "drizzle-orm";
|
||||
import type { Db } from "@paperclipai/db";
|
||||
import { connectionGrants, toolConnections } from "@paperclipai/db";
|
||||
import { companies, connectionGrants, toolConnections } from "@paperclipai/db";
|
||||
import { conflict, HttpError, notFound, unprocessable } from "../../errors.js";
|
||||
import { PaperclipCloudConnectorError, type PaperclipCloudConnector } from "../paperclip-cloud-connector.js";
|
||||
|
||||
@@ -235,19 +235,27 @@ export function createGitHubGrantContinuity(db: Db, { now, currentCloudConnector
|
||||
eq(toolConnections.id, connection.id), eq(toolConnections.companyId, connection.companyId),
|
||||
)).for("update").limit(1);
|
||||
if (!liveConnection || liveConnection.status !== "active" || !liveConnection.enabled) return current;
|
||||
const [company] = await tx.select({ status: companies.status }).from(companies)
|
||||
.where(eq(companies.id, connection.companyId)).for("share").limit(1);
|
||||
if (company?.status !== "active") return current;
|
||||
const github = current.providerTenant?.github;
|
||||
if (!github?.bindingSync || github.bindingSync.id !== grant.providerTenant?.github?.bindingSync?.id) return current;
|
||||
const subject = current.kind === "agent" && current.subjectAgentId ? `agent:${current.subjectAgentId}`
|
||||
: current.kind === "user" ? current.subjectUserId : null;
|
||||
if (!subject) throw unprocessable("GitHub webhook subject is unavailable");
|
||||
for (const installationId of github.installationIds) await cloudConnector.setWebhookBinding({
|
||||
subject, companyId: connection.companyId, id: `${current.id}_${installationId}`,
|
||||
installationId, connectionId: connection.id, grantId: current.id, active: true, accessToken: token!,
|
||||
});
|
||||
for (const installationId of github.bindingSync.removeInstallationIds) await cloudConnector.setWebhookBinding({
|
||||
subject, companyId: connection.companyId, id: `${current.id}_${installationId}`,
|
||||
installationId, connectionId: connection.id, grantId: current.id, active: false,
|
||||
});
|
||||
const changes = [
|
||||
...github.installationIds.map(installationId => ({ installationId, active: true as const })),
|
||||
...github.bindingSync.removeInstallationIds.map(installationId => ({ installationId, active: false as const })),
|
||||
];
|
||||
for (let offset = 0; offset < changes.length; offset += 4) {
|
||||
const outcomes = await Promise.allSettled(changes.slice(offset, offset + 4).map(change => cloudConnector.setWebhookBinding({
|
||||
subject, companyId: connection.companyId, id: `${current.id}_${change.installationId}`,
|
||||
installationId: change.installationId, connectionId: connection.id, grantId: current.id,
|
||||
...change.active ? { active: true, accessToken: token! } : { active: false },
|
||||
})));
|
||||
const failure = outcomes.find(outcome => outcome.status === "rejected");
|
||||
if (failure?.status === "rejected") throw failure.reason;
|
||||
}
|
||||
await notifyGitHubCredentialChange(tx, connection);
|
||||
const [settled] = await tx.update(connectionGrants).set({
|
||||
providerTenant: sql`${connectionGrants.providerTenant} #- '{github,bindingSync}' #- '{github,continuityRetry}'`,
|
||||
|
||||
Reference in new issue
Block a user