mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-20 15:09:57 +02:00
Separate GitHub bot opt-in and add review detail pages
Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
7c1d2503db
commit
7dcdbe7eb6
36 files changed
+647
-204
No files matched your search
@@ -38,6 +38,7 @@ import { materializeNativeInteractionResponses } from "../services/native-runtim
|
||||
import { toolAccessService } from "../services/tool-access.js";
|
||||
import { toolAccessPolicyService } from "../services/tool-access-policy.js";
|
||||
import { connectionIntentService } from "../services/connection-intents.js";
|
||||
import { instanceSettingsService } from "../services/instance-settings.js";
|
||||
import {
|
||||
getEmbeddedPostgresTestSupport,
|
||||
startEmbeddedPostgresTestDatabase,
|
||||
@@ -198,6 +199,29 @@ describeEmbeddedPostgres("connectionIntentService", () => {
|
||||
expect(serialized).not.toContain(claims.sub);
|
||||
});
|
||||
|
||||
it("discovers GitHub review bots independently from chat connectors", async () => {
|
||||
const settings = instanceSettingsService(db);
|
||||
const previous = await settings.getExperimental();
|
||||
const service = connectionIntentService(db);
|
||||
try {
|
||||
await settings.updateExperimental({ enableChatConnectors: false, enableGitHubReviewBots: true });
|
||||
expect((await service.search(claims, "github")).results).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ service: "github-code-review-bot" }),
|
||||
expect.objectContaining({ service: "github" }),
|
||||
]));
|
||||
expect((await service.search(claims, "slack")).results.some(item =>
|
||||
item.methods.some(method => method.purpose === "channel"))).toBe(false);
|
||||
await settings.updateExperimental({ enableChatConnectors: true, enableGitHubReviewBots: false });
|
||||
expect((await service.search(claims, "github")).results.some(item =>
|
||||
item.service === "github-code-review-bot")).toBe(false);
|
||||
} finally {
|
||||
await settings.updateExperimental({
|
||||
enableChatConnectors: previous.enableChatConnectors,
|
||||
enableGitHubReviewBots: previous.enableGitHubReviewBots,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("creates one addressed request, resolves after install, and then reports ready for the responsible user", async () => {
|
||||
const service = connectionIntentService(db);
|
||||
const first = await service.request(claims, "notion");
|
||||
|
||||
@@ -16,6 +16,11 @@ describe("instance settings service", () => {
|
||||
const disabled = applyExperimentalSettingsPatch(enabled, { enableChatConnectors: false });
|
||||
expect(disabled).toMatchObject({ enableApps: true, enableChatConnectors: false });
|
||||
});
|
||||
it("keeps GitHub review bots independent across legacy normalization and persistence", () => {
|
||||
expect(normalizeExperimentalSettings({ enableChatConnectors: true })).toMatchObject({ enableChatConnectors: true, enableGitHubReviewBots: false });
|
||||
const saved = applyExperimentalSettingsPatch({}, { enableGitHubReviewBots: true });
|
||||
expect(normalizeExperimentalSettings(JSON.parse(JSON.stringify(saved)))).toMatchObject({ enableChatConnectors: false, enableGitHubReviewBots: true });
|
||||
});
|
||||
it("ignores retired experimental flags without resetting current settings", () => {
|
||||
expect(normalizeExperimentalSettings({
|
||||
enableEnvironments: true,
|
||||
@@ -48,6 +53,7 @@ describe("instance settings service", () => {
|
||||
enablePublicMcp: false,
|
||||
enableCombinedInboxTasks: false,
|
||||
enableChatConnectors: false,
|
||||
enableGitHubReviewBots: false,
|
||||
enableMemoryConnectors: false,
|
||||
enableConferenceRoomChat: false,
|
||||
enableClassicTaskInterface: false,
|
||||
|
||||
@@ -65,6 +65,19 @@ describe("chat identity-link preview authority", () => {
|
||||
expect(f.requestIdentityAccess).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each(["github", "slack"])("uses the token’s verified provider when only GitHub bots are enabled (%s)", async (provider) => {
|
||||
settings.getExperimental.mockResolvedValue({ enableChatConnectors: false, enableGitHubReviewBots: true });
|
||||
const f = fixture();
|
||||
f.previewIdentityLink.mockResolvedValue({ ...preview, provider } as typeof preview);
|
||||
await request(f.app).get("/api/chat-identity-links/preview").query({ token }).expect(provider === "github" ? 200 : 403);
|
||||
await request(f.app).post("/api/chat-identity-links/confirm").send({ token }).expect(provider === "github" ? 200 : 403);
|
||||
expect(f.confirmIdentityLink.mock.calls.length).toBe(provider === "github" ? 1 : 0);
|
||||
});
|
||||
it("does not admit GitHub links under chat-only opt-in", async () => {
|
||||
const f = fixture();
|
||||
f.previewIdentityLink.mockResolvedValue({ ...preview, provider: "github" } as unknown as typeof preview);
|
||||
await request(f.app).get("/api/chat-identity-links/preview").query({ token }).expect(403);
|
||||
});
|
||||
it("returns the preview to a Board member of its exact company", async () => {
|
||||
const { app, previewIdentityLink } = fixture();
|
||||
const response = await request(app)
|
||||
|
||||
@@ -92,16 +92,27 @@ export function chatChannelRoutes(db: Db, options: ChatChannelRouteOptions) {
|
||||
const access = accessService(db);
|
||||
const github = githubChatManagementService(db, options.fetch);
|
||||
|
||||
async function assertIdentityLinkAccess(req: ExpressRequest): Promise<string> {
|
||||
async function assertIdentityLinkAccess(req: ExpressRequest) {
|
||||
assertBoard(req);
|
||||
const userId = actorUserId(req);
|
||||
if (!userId) throw badRequest("A signed-in Paperclip user is required");
|
||||
// Enforce rollout here: invited nonmembers cannot read the board's
|
||||
// experimental-settings API. A private token never bypasses this gate.
|
||||
if (!(await instanceSettingsService(db).getExperimental()).enableChatConnectors) {
|
||||
const experimental = await instanceSettingsService(db).getExperimental();
|
||||
if (!experimental.enableChatConnectors && !experimental.enableGitHubReviewBots) {
|
||||
throw forbidden("Chat connectors are not enabled on this instance");
|
||||
}
|
||||
return userId;
|
||||
return { userId, experimental };
|
||||
}
|
||||
|
||||
function assertIdentityProviderEnabled(
|
||||
provider: string,
|
||||
experimental: { enableChatConnectors: boolean; enableGitHubReviewBots: boolean },
|
||||
) {
|
||||
const enabled = provider === "agentmail" || (provider === "github"
|
||||
? experimental.enableGitHubReviewBots
|
||||
: experimental.enableChatConnectors);
|
||||
if (!enabled) throw forbidden("This connector is not enabled on this instance");
|
||||
}
|
||||
|
||||
async function assertConnectionManager(
|
||||
@@ -353,18 +364,22 @@ export function chatChannelRoutes(db: Db, options: ChatChannelRouteOptions) {
|
||||
"/chat-identity-links/confirm",
|
||||
validate(confirmChatIdentityLinkSchema),
|
||||
async (req, res) => {
|
||||
const userId = await assertIdentityLinkAccess(req);
|
||||
const { userId, experimental } = await assertIdentityLinkAccess(req);
|
||||
const preview = await service.previewIdentityLink(req.body.token, userId);
|
||||
assertIdentityProviderEnabled(preview.provider, experimental);
|
||||
res.json(await service.confirmIdentityLink(req.body.token, userId));
|
||||
},
|
||||
);
|
||||
|
||||
router.post("/chat-identity-links/request-access", validate(confirmChatIdentityLinkSchema), async (req, res) => {
|
||||
const userId = await assertIdentityLinkAccess(req);
|
||||
const { userId, experimental } = await assertIdentityLinkAccess(req);
|
||||
const preview = await service.previewIdentityLink(req.body.token, userId);
|
||||
assertIdentityProviderEnabled(preview.provider, experimental);
|
||||
res.json(await service.requestIdentityAccess(req.body.token, userId, req.ip ?? "unknown"));
|
||||
});
|
||||
|
||||
router.get("/chat-identity-links/preview", async (req, res) => {
|
||||
const userId = await assertIdentityLinkAccess(req);
|
||||
const { userId, experimental } = await assertIdentityLinkAccess(req);
|
||||
const token = typeof req.query.token === "string" ? req.query.token : "";
|
||||
if (token.length < 32 || token.length > 4096)
|
||||
throw badRequest("A valid identity-link token is required");
|
||||
@@ -379,6 +394,7 @@ export function chatChannelRoutes(db: Db, options: ChatChannelRouteOptions) {
|
||||
req, res, Promise.resolve(invitation), "Identity-link request not found",
|
||||
);
|
||||
if (!preview) return;
|
||||
assertIdentityProviderEnabled(preview.provider, experimental);
|
||||
res.json(preview);
|
||||
});
|
||||
|
||||
|
||||
@@ -409,7 +409,8 @@ export function connectionIntentService(db: Db) {
|
||||
const candidates: Array<{ item: ConnectionSearchResultItem; score: number; nameScore: number }> = [];
|
||||
const authorizedCatalogs = new Map<string, Awaited<ReturnType<typeof indexedCatalog>>>();
|
||||
const discoveryMethods = (app: (typeof APP_STORE_DEFINITIONS)[number]) => getAvailableConnectionMethods(app)
|
||||
.filter(method => method.purpose !== "channel" || app.slug === "agentmail" || settings.enableChatConnectors);
|
||||
.filter(method => method.purpose !== "channel" || app.slug === "agentmail"
|
||||
|| (method.provider === "github" ? settings.enableGitHubReviewBots : settings.enableChatConnectors));
|
||||
const services = [...APP_STORE_DEFINITIONS.filter(app => discoveryMethods(app).length).map((app) => app.slug),
|
||||
...inventory.connections.filter((connection) =>
|
||||
sourceSlugForConnection(connection, inventory.applicationsById)?.startsWith("connection:")
|
||||
|
||||
@@ -235,6 +235,7 @@ export function normalizeExperimentalSettings(raw: unknown): InstanceExperimenta
|
||||
enableMcpAggregators: true,
|
||||
enablePublicMcp: parsed.data.enablePublicMcp ?? false,
|
||||
enableChatConnectors: parsed.data.enableChatConnectors ?? false,
|
||||
enableGitHubReviewBots: parsed.data.enableGitHubReviewBots ?? false,
|
||||
enableMemoryConnectors: parsed.data.enableMemoryConnectors ?? false,
|
||||
enablePipelines: parsed.data.enablePipelines ?? false,
|
||||
enableCases: parsed.data.enableCases ?? false,
|
||||
@@ -281,6 +282,7 @@ export function normalizeExperimentalSettings(raw: unknown): InstanceExperimenta
|
||||
enableMcpAggregators: true,
|
||||
enablePublicMcp: false,
|
||||
enableChatConnectors: false,
|
||||
enableGitHubReviewBots: false,
|
||||
enableMemoryConnectors: false,
|
||||
enablePipelines: false,
|
||||
enableCases: false,
|
||||
|
||||
Reference in new issue
Block a user