import { applyPendingMigrations } from "@paperclipai/db"; import { resolveExternalTestDatabaseUrl } from "./helpers/external-test-database.js"; import { mkdtemp, rm } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { createPublicMcpTransfers } from "../services/public-mcp/file-transfers.js"; import { projectRoutes } from "../routes/projects.js"; import { agentRoutes } from "../routes/agents.js"; import { companySkillRoutes } from "../routes/company-skills.js"; import { MAX_ATTACHMENT_BYTES } from "../attachment-types.js"; import { mcpAttachmentUploads, mcpFileTickets, principalPermissionGrants, heartbeatRuns, issueAccessGrants } from "@paperclipai/db"; import { createHmac, createHash, randomBytes, randomUUID } from "node:crypto"; import express, { type Request } from "express"; import request from "supertest"; import { and, eq, sql } from "drizzle-orm"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { activityLog, mcpEventAdmissions, mcpEventDeliveries, mcpEventSubscriptions, createDb, authUsers, companies, companyLogos, assets, companyMemberships, mcpOauthTokens, mcpOauthRequests, mcpOauthGrants, mcpOauthClients, mcpOauthDeviceRequests, mcpOauthMetadataAdmissions, mcpMutationReceipts, agents, issues, issueComments, instanceUserRoles } from "@paperclipai/db"; import { instanceSettingsService } from "../services/instance-settings.js"; import { createPublicMcpOAuth, publicMcpConfig, hashMcpSecret, DEVICE_GRANT, McpOAuthError } from "../services/public-mcp/oauth.js"; import { McpApiError, createMcpApiDispatch, createPublicMcpExecutor, publicMcpCapabilities } from "../services/public-mcp/capabilities.js"; import { publicMcpIngressRoutes, publicMcpManagementRoutes } from "../routes/public-mcp.js"; import { authorizationService } from "../services/authorization.js"; import { issueRoutes } from "../routes/issues.js"; import { activityRoutes } from "../routes/activity.js"; import { documentService } from "../services/documents.js"; import { createStorageService } from "../storage/service.js"; import { createLocalDiskStorageProvider } from "../storage/local-disk-provider.js"; import * as assignmentWakeups from "../services/issue-assignment-wakeup.js"; import { assertCompanyAccess } from "../routes/authz.js"; import { actorMiddleware } from "../middleware/auth.js"; import { boardMutationGuard } from "../middleware/board-mutation-guard.js"; import { cloudWarmStandbyMiddleware } from "../middleware/cloud-warm-standby.js"; import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase } from "./helpers/embedded-postgres.js"; import { createPublicMcpEvents, publicMcpEventDefinitions } from "../services/public-mcp/events.js"; import { createDotRunnerMcpBridge } from "../services/public-mcp/dot-runner.js"; import { DotHarnessDriver } from "../../../packages/paperclip-runner/src/drivers/dot/dot-harness-driver.js"; import { HarnessDriverBackend } from "../../../packages/paperclip-runner/src/backends/harness-driver-backend.js"; import { eventFetch, signingKey, type EventFetch } from "../services/public-mcp/event-webhooks.js"; const externalUrl = resolveExternalTestDatabaseUrl("PAPERCLIP_PUBLIC_MCP_TEST_DATABASE_URL"); const support = externalUrl ? { supported: true, reason: undefined } : await getEmbeddedPostgresTestSupport(); if (!support.supported) console.warn(`Public MCP database checks unavailable: ${support.reason}`); const config = { origin: "https://paperclip.example", resource: "https://paperclip.example/mcp/paperclip" }; const redirectUri = "https://client.example/callback"; const verifier = randomBytes(32).toString("base64url"); const challenge = createHash("sha256").update(verifier).digest("base64url"); describe("public MCP configuration", () => { it("uses the configured origin independently of the retired environment gate", () => { expect(publicMcpConfig({})).toBeNull(); expect(() => publicMcpConfig({ PAPERCLIP_PUBLIC_URL: "http://example.com" })).toThrow(); expect(publicMcpConfig({ PAPERCLIP_PUBLIC_URL: "http://localhost:3100" })?.resource).toBe("http://localhost:3100/mcp/paperclip"); expect(publicMcpConfig({}, "https://paperclip.example")?.origin).toBe("https://paperclip.example"); expect(publicMcpConfig({ PAPERCLIP_PUBLIC_MCP_ENABLED: "true" })).toBeNull(); expect(publicMcpCapabilities.map((c) => c.name)).not.toEqual(expect.arrayContaining(["run_tool", "call_api"])); }); }); describe.skipIf(!support.supported)("public MCP OAuth and tool boundary", () => { let temp: Awaited>; let db: ReturnType; let oauth: ReturnType; beforeAll(async () => { vi.stubEnv("PAPERCLIP_SECRETS_MASTER_KEY", randomBytes(32).toString("base64")); if (externalUrl) await applyPendingMigrations(externalUrl); else temp = await startEmbeddedPostgresTestDatabase("paperclip-public-mcp-"); db = createDb(externalUrl ?? temp.connectionString); oauth = createPublicMcpOAuth(db, config); expect(await oauth.isEnabled()).toBe(false); await instanceSettingsService(db).updateExperimental({ enablePublicMcp: true }); }, 90000); afterAll(async () => { await db?.$client.end({ timeout: 1 }); await temp?.cleanup(); vi.unstubAllEnvs(); }); it("keeps public MCP and discovery out of SQL and SPA fallback until Cloud claim", async () => { let standby = true; const app = express(); const ui = express.Router(); ui.get(/.*/, (_req, res) => res.type("html").send("Paperclip UI")); app.use(cloudWarmStandbyMiddleware(() => standby, express.Router(), ui)); app.use(publicMcpIngressRoutes(oauth, vi.fn())); const select = vi.spyOn(db, "select"); try { for (const path of [ "/mcp/setup", "/mcp/setup.md", "/mcp/paperclip", "/mcp/oauth/authorize", "/.well-known/oauth-protected-resource", "/.well-known/oauth-protected-resource/mcp/paperclip", "/.well-known/oauth-authorization-server", ]) { for (const method of ["get", "head"] as const) { expect((await request(app)[method](path).set("authorization", "Bearer unclaimed")).status).toBe(503); } } for (const path of ["/mcp/paperclip", "/mcp/oauth/register", "/mcp/oauth/device_authorization", "/mcp/oauth/token", "/mcp/oauth/revoke"]) { const response = await request(app).post(path).send({}); expect(response.status).toBe(503); expect(response.body).toEqual({ error: "workspace_unclaimed" }); } // Consent routes still serve the UI shell without resolving a session. expect((await request(app).get("/mcp-connect/request")).status).toBe(200); expect((await request(app).get("/mcp-device")).status).toBe(200); expect(select).not.toHaveBeenCalled(); standby = false; const metadata = await request(app).get("/.well-known/oauth-protected-resource/mcp/paperclip"); expect(metadata.status).toBe(200); expect(metadata.body.resource).toBe(config.resource); expect(select).toHaveBeenCalled(); expect((await request(app).get("/mcp/setup.md")).status).toBe(200); expect((await request(app).post("/mcp/paperclip")).status).toBe(401); } finally { select.mockRestore(); } }); async function fixture(role = "member", write = true, configure = false) { const userId = randomUUID(); await db.insert(authUsers).values({ id: userId, name: "Human", email: userId + "@example.com", createdAt: new Date(), updatedAt: new Date() }); const [company] = await db.insert(companies).values({ name: "Team", issuePrefix: "M" + randomBytes(4).toString("hex") }).returning(); const [membership] = await db.insert(companyMemberships).values({ companyId: company!.id, principalType: "user", principalId: userId, membershipRole: role, status: "active" }).returning(); const actor: Request["actor"] = { type: "board", source: "session", userId }; const client = await oauth.register({ client_name: "Test client", redirect_uris: [redirectUri] }, randomUUID()); const url = await oauth.authorize({ client_id: client.client_id, redirect_uri: redirectUri, resource: config.resource, scope: "paperclip:read paperclip:write paperclip:configure offline_access", state: "state", response_type: "code", code_challenge: challenge, code_challenge_method: "S256" }); const id = url.split("/").at(-1)!; const consent = await oauth.consent(id, actor, { decision: "approve", companyId: company!.id, allowWrites: write, allowConfiguration: configure }); const code = new URL(consent.redirectUrl).searchParams.get("code")!; const exchange = { grant_type: "authorization_code", client_id: client.client_id, redirect_uri: redirectUri, resource: config.resource, code, code_verifier: verifier }; const tokens = await oauth.token(exchange); return { actor, company: company!, membership: membership!, client, tokens, exchange }; } it("returns only the authorizing person’s profile with their connection list", async () => { const f = await fixture(); const other = await fixture(); await db.update(authUsers).set({ name: "Dotta", image: "https://avatars.example/dotta.png" }).where(eq(authUsers.id, f.actor.userId!)); const rows = await oauth.listConnections(f.actor.userId!); expect(rows).toHaveLength(1); expect(rows[0]).toMatchObject({ companyId: f.company.id, user: { name: "Dotta", image: "https://avatars.example/dotta.png" } }); expect(JSON.stringify(rows)).not.toContain(other.actor.userId!); expect(JSON.stringify(rows)).not.toContain("@example.com"); await oauth.revokeConnection(rows[0]!.id, f.actor.userId!); expect((await oauth.listConnections(f.actor.userId!))[0]?.revokedAt).not.toBeNull(); }); async function deviceFixture() { const f = await fixture(); const client = await oauth.register({ client_name: "Device client", redirect_uris: [], grant_types: [DEVICE_GRANT, "refresh_token"], response_types: [] }, randomUUID()); const codes = await oauth.deviceAuthorize({ client_id: client.client_id, resource: config.resource, scope: "paperclip:read paperclip:write offline_access", company_id: f.company.id }, randomUUID()); const exchange = { grant_type: DEVICE_GRANT, client_id: client.client_id, resource: config.resource, device_code: codes.device_code }; return { ...f, deviceClient: client, codes, deviceExchange: exchange }; } it("keeps device codes private and atomically binds approval to the human and company", async () => { const f = await deviceFixture(); const replica = createPublicMcpOAuth(db, config); const unauthenticated = await replica.describeDevice(f.codes.user_code, { type: "none" }); expect(unauthenticated.companies).toEqual([]); expect(unauthenticated.requiresSignIn).toBe(true); const stored = await db.select().from(mcpOauthDeviceRequests).where(eq(mcpOauthDeviceRequests.clientId, f.deviceClient.client_id)); expect(JSON.stringify(stored)).not.toContain(f.codes.device_code); expect(JSON.stringify(stored)).not.toContain(f.codes.user_code); await expect(replica.consentDevice(f.codes.user_code, f.actor, { decision: "approve", companyId: randomUUID(), allowWrites: true })).rejects.toMatchObject({ status: 403 }); await replica.consentDevice(f.codes.user_code, f.actor, { decision: "approve", companyId: f.company.id, allowWrites: true }); await expect(replica.token({ ...f.deviceExchange, resource: "https://other.example/mcp" })).rejects.toThrow(); const results = await Promise.allSettled([oauth.token(f.deviceExchange), replica.token(f.deviceExchange)]); expect(results.filter(r => r.status === "fulfilled")).toHaveLength(1); const result = results.find(r => r.status === "fulfilled") as PromiseFulfilledResult>>; const principal = await oauth.authenticate(result.value.access_token); expect(principal.actor.userId).toBe(f.actor.userId); expect(principal.grant.companyId).toBe(f.company.id); await replica.revokeConnection(principal.grant.id, f.actor.userId!); await expect(oauth.authenticate(result.value.access_token)).rejects.toThrow(); await expect(oauth.token({ grant_type: "refresh_token", client_id: f.deviceClient.client_id, resource: config.resource, refresh_token: result.value.refresh_token })).rejects.toThrow(); }); it("persists polling backoff across replicas and handles denied and expired device requests", async () => { const f = await deviceFixture(); await expect(oauth.token(f.deviceExchange)).rejects.toMatchObject({ code: "authorization_pending" }); const replica = createPublicMcpOAuth(db, config); await expect(replica.token(f.deviceExchange)).rejects.toMatchObject({ code: "slow_down" }); const [stored] = await db.select().from(mcpOauthDeviceRequests).where(eq(mcpOauthDeviceRequests.clientId, f.deviceClient.client_id)); expect(stored!.intervalSeconds).toBe(10); await replica.consentDevice(f.codes.user_code, f.actor, { decision: "deny", allowWrites: false }); await expect(oauth.token(f.deviceExchange)).rejects.toMatchObject({ code: "access_denied" }); await db.update(mcpOauthDeviceRequests).set({ expiresAt: new Date(0) }).where(eq(mcpOauthDeviceRequests.id, stored!.id)); await expect(oauth.token(f.deviceExchange)).rejects.toMatchObject({ code: "expired_token" }); }); it("accepts verified CIMD public clients without dynamic registration", async () => { const f = await fixture(); const clientId = "https://client.example/public-mcp.json"; const cimd = createPublicMcpOAuth(db, config, { metadataFetch: async () => new Response(JSON.stringify({ client_id: clientId, client_name: "CIMD client", redirect_uris: [redirectUri], grant_types: ["authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:jwt-bearer"] }), { headers: { "content-type": "application/json" } }) }); const id = (await cimd.authorize({ client_id: clientId, redirect_uri: redirectUri, response_type: "code", resource: config.resource, code_challenge: challenge, code_challenge_method: "S256" })).split("/").at(-1)!; expect((await cimd.describeRequest(id, f.actor, null)).clientOrigin).toBe("https://client.example"); const consent = await cimd.consent(id, f.actor, { decision: "approve", companyId: f.company.id, allowWrites: false }); expect(new URL(consent.redirectUrl).searchParams.get("iss")).toBe(config.origin); const tokens = await cimd.token({ grant_type: "authorization_code", client_id: clientId, redirect_uri: redirectUri, resource: config.resource, code: new URL(consent.redirectUrl).searchParams.get("code"), code_verifier: verifier }); expect((await cimd.authenticate(tokens.access_token)).grant.companyId).toBe(f.company.id); const [registered] = await db.select().from(mcpOauthClients).where(eq(mcpOauthClients.id, clientId)); expect(registered!.grantTypes).toEqual(["authorization_code", "refresh_token"]); await expect(cimd.token({ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", client_id: clientId, resource: config.resource })).rejects.toMatchObject({ code: "unsupported_grant_type" }); }); it("admits CIMD clients atomically under durable source quotas and cleans stale registrations", async () => { const source = "cimd-quota-source"; const prefix = `https://quota-${randomUUID()}.example/`; const cimd = createPublicMcpOAuth(db, config, { metadataFetch: async url => new Response(JSON.stringify({ client_id: url.toString(), client_name: "Quota client", redirect_uris: [redirectUri] }), { headers: { "content-type": "application/json" } }) }); const input = { client_id: prefix + "rejected.json", redirect_uri: redirectUri, response_type: "code", resource: "https://wrong.example/mcp", code_challenge: challenge, code_challenge_method: "S256" }; await expect(cimd.authorize(input, source)).rejects.toThrow(); expect(await db.select().from(mcpOauthClients).where(eq(mcpOauthClients.id, input.client_id))).toHaveLength(0); const old = prefix + "old.json"; await db.insert(mcpOauthClients).values({ id: old, name: "Expired", redirectUris: [redirectUri], createdAt: new Date(Date.now() - 70 * 60_000) }); const ids = Array.from({ length: 7 }, (_, i) => prefix + i + ".json"); try { for (const client_id of ids.slice(0, 6)) await cimd.authorize({ ...input, client_id, resource: config.resource }, source); await expect(cimd.authorize({ ...input, client_id: ids[6], resource: config.resource }, source)).rejects.toMatchObject({ status: 429 }); expect(await db.select().from(mcpOauthClients).where(eq(mcpOauthClients.id, ids[6]!))).toHaveLength(0); expect(await db.select().from(mcpOauthClients).where(eq(mcpOauthClients.id, old))).toHaveLength(0); } finally { for (const id of ids) { await db.delete(mcpOauthRequests).where(eq(mcpOauthRequests.clientId, id)); await db.delete(mcpOauthClients).where(eq(mcpOauthClients.id, id)); } } }); describe("CIMD network admission", () => { beforeEach(async () => { await db.delete(mcpOauthMetadataAdmissions); }); afterEach(async () => { await db.delete(mcpOauthMetadataAdmissions); }); const input = (index: number) => ({ client_id: `https://admission.example/${index}.json`, redirect_uri: redirectUri, response_type: "code", resource: config.resource, code_challenge: challenge, code_challenge_method: "S256" }); const metadata = (url: URL) => new Response(JSON.stringify({ client_id: url.toString(), client_name: "Admission test", redirect_uris: [redirectUri], grant_types: ["authorization_code", DEVICE_GRANT] }), { headers: { "content-type": "application/json" } }); it("rejects wrong resources, scopes and disabled connections before fetching", async () => { const fetcher = vi.fn(async (url: URL) => metadata(url)); const client = createPublicMcpOAuth(db, config, { metadataFetch: fetcher }); for (const override of [{ resource: "https://wrong.example/mcp" }, { scope: "paperclip:read invalid" }]) { await expect(client.authorize({ ...input(0), ...override })).rejects.toThrow(); await expect(client.deviceAuthorize({ ...input(0), ...override })).rejects.toThrow(); } await instanceSettingsService(db).updateExperimental({ enablePublicMcp: false }); try { await expect(client.authorize(input(0))).rejects.toMatchObject({ status: 503 }); await expect(client.deviceAuthorize(input(0))).rejects.toMatchObject({ status: 503 }); } finally { await instanceSettingsService(db).updateExperimental({ enablePublicMcp: true }); } expect(fetcher).not.toHaveBeenCalled(); expect(await db.select().from(mcpOauthMetadataAdmissions)).toHaveLength(0); }); it.each(["fetch failure", "invalid metadata", "redirect mismatch"])("retains admission after %s across replicas and both grants", async mode => { const fetcher = vi.fn(async (url: URL) => { if (mode === "fetch failure") throw new Error("Remote unavailable"); if (mode === "invalid metadata") return new Response("{}", { headers: { "content-type": "application/json" } }); // Browser rejects the callback; device rejects the unsupported grant. return new Response(JSON.stringify({ client_id: url.toString(), client_name: "Mismatch", redirect_uris: ["https://other.example/callback"], grant_types: ["authorization_code"] }), { headers: { "content-type": "application/json" } }); }); const clients = [createPublicMcpOAuth(db, config, { metadataFetch: fetcher }), createPublicMcpOAuth(db, config, { metadataFetch: fetcher })]; const outcomes = await Promise.allSettled(Array.from({ length: 12 }, (_, i) => i % 2 ? clients[1]!.deviceAuthorize(input(i), "same-source") : clients[0]!.authorize(input(i), "same-source"))); expect(fetcher).toHaveBeenCalledTimes(6); expect(outcomes.every(result => result.status === "rejected")).toBe(true); expect(outcomes.filter(result => result.status === "rejected" && result.reason.status === 429)).toHaveLength(6); const receipts = await db.select().from(mcpOauthMetadataAdmissions); expect(receipts).toHaveLength(6); expect(receipts.every(row => row.sourceHash === hashMcpSecret(config.resource + ":same-source"))).toBe(true); expect(JSON.stringify(receipts)).not.toContain("same-source"); // Expired receipts are pruned on the next attempt, including after restart. await db.update(mcpOauthMetadataAdmissions).set({ expiresAt: new Date(0) }); const restarted = createPublicMcpOAuth(db, config, { metadataFetch: fetcher }); await expect(restarted.authorize(input(99), "same-source")).rejects.not.toMatchObject({ status: 429 }); expect(fetcher).toHaveBeenCalledTimes(7); expect(await db.select().from(mcpOauthMetadataAdmissions)).toHaveLength(1); }); it("reports admission storage failures as retryable and never fetches without admission", async () => { const fetcher = vi.fn(async (url: URL) => metadata(url)); const client = createPublicMcpOAuth(db, config, { metadataFetch: fetcher }); const storageFailure = vi.spyOn(db, "transaction").mockRejectedValueOnce(new Error("Database unavailable")); try { await expect(client.authorize(input(0), "storage-failure")).rejects.toMatchObject({ code: "temporarily_unavailable", status: 503 }); expect(fetcher).not.toHaveBeenCalled(); } finally { storageFailure.mockRestore(); } expect(await db.select().from(mcpOauthMetadataAdmissions)).toHaveLength(0); await expect(client.authorize(input(0), "storage-failure")).resolves.toContain("/mcp-connect/"); expect(fetcher).toHaveBeenCalledTimes(1); }); it("lets users behind one proxy reuse verified metadata beyond the network quota", async () => { const clientId = `https://shared-proxy-${randomUUID()}.example/client.json`; const fetcher = vi.fn(async (url: URL) => metadata(url)); const clients = [createPublicMcpOAuth(db, config, { metadataFetch: fetcher }), createPublicMcpOAuth(db, config, { metadataFetch: fetcher })]; for (let i = 0; i < 9; i++) { const request = { ...input(i), client_id: clientId }; await expect(clients[i % 2]!.authorize(request, "shared-proxy")).resolves.toContain("/mcp-connect/"); await expect(clients[i % 2]!.deviceAuthorize(request, "shared-proxy")).resolves.toHaveProperty("user_code"); } expect(fetcher).toHaveBeenCalledTimes(2); expect(await db.select().from(mcpOauthMetadataAdmissions)).toHaveLength(2); }); it("caps unique-source failures globally without blocking registered clients", async () => { const fetcher = vi.fn(async () => { throw new Error("Remote unavailable"); }); const clients = [createPublicMcpOAuth(db, config, { metadataFetch: fetcher }), createPublicMcpOAuth(db, config, { metadataFetch: fetcher })]; const outcomes = await Promise.allSettled(Array.from({ length: 65 }, (_, i) => i % 2 ? clients[1]!.deviceAuthorize(input(i), `source-${i}`) : clients[0]!.authorize(input(i), `source-${i}`))); expect(fetcher).toHaveBeenCalledTimes(60); expect(outcomes.filter(result => result.status === "rejected" && result.reason.status === 429)).toHaveLength(5); expect(await db.select().from(mcpOauthMetadataAdmissions)).toHaveLength(60); const registered = await oauth.register({ client_name: "Local client", redirect_uris: [redirectUri] }, randomUUID()); await expect(clients[0]!.authorize({ ...input(99), client_id: registered.client_id })).resolves.toContain("/mcp-connect/"); expect(fetcher).toHaveBeenCalledTimes(60); }); it("commits admission before slow network work without holding the shared lock", async () => { let release!: () => void; const pending = new Promise(resolve => { release = resolve; }); const fetcher = vi.fn(async () => { await pending; throw new Error("Remote timeout"); }); const clients = [createPublicMcpOAuth(db, config, { metadataFetch: fetcher }), createPublicMcpOAuth(db, config, { metadataFetch: fetcher })]; const requests = Array.from({ length: 6 }, (_, i) => clients[i % 2]!.authorize(input(i), "slow-source")); const outcomes = Promise.allSettled(requests); try { await expect.poll(() => fetcher.mock.calls.length).toBe(6); await expect(clients[1]!.deviceAuthorize(input(7), "slow-source")).rejects.toMatchObject({ status: 429 }); expect(await db.select().from(mcpOauthMetadataAdmissions)).toHaveLength(6); } finally { release(); await outcomes; } }); }); it("requires exact redirect, resource, S256 PKCE and real browser consent", async () => { await expect(oauth.register({ client_name: "bad", redirect_uris: ["https://u:p@example.com"] })).rejects.toThrow(); const client = await oauth.register({ client_name: "Client", redirect_uris: [redirectUri] }); const input = { client_id: client.client_id, redirect_uri: redirectUri, response_type: "code", resource: config.resource, code_challenge: challenge, code_challenge_method: "S256" }; await expect(oauth.authorize({ ...input, redirect_uri: "https://evil.example" })).rejects.toThrow(); await expect(oauth.authorize({ ...input, resource: "https://other.example/mcp/paperclip" })).rejects.toThrow(); await expect(oauth.authorize({ ...input, code_challenge_method: "plain" })).rejects.toThrow(); const id = (await oauth.authorize(input)).split("/").at(-1)!; await expect(oauth.consent(id, { type: "board", source: "local_implicit", userId: "board" }, { decision: "approve", companyId: randomUUID(), allowWrites: true })).rejects.toThrow(); const f = await fixture(); const pendingId = (await oauth.authorize(input)).split("/").at(-1)!; const pendingConsent = await oauth.consent(pendingId, f.actor, { decision: "approve", companyId: f.company.id, allowWrites: false }); const pendingCode = new URL(pendingConsent.redirectUrl).searchParams.get("code")!; const pendingExchange = { grant_type: "authorization_code", client_id: client.client_id, resource: config.resource, redirect_uri: redirectUri, code: pendingCode, code_verifier: verifier }; await expect(oauth.token({ ...pendingExchange, code_verifier: randomBytes(32).toString("base64url") })).rejects.toThrow(); await expect(oauth.token(pendingExchange)).resolves.toHaveProperty("access_token"); expect(new URL((await oauth.authorize(input)))).toHaveProperty("origin", config.origin); await expect(oauth.token(f.exchange)).rejects.toThrow(); await expect(oauth.token({ ...f.exchange, code_verifier: randomBytes(32).toString("base64url") })).rejects.toThrow(); const principal = await oauth.authenticate(f.tokens.access_token); expect(principal.actor).toMatchObject({ type: "board", source: "mcp_oauth", userId: f.actor.userId, companyIds: [f.company.id], isInstanceAdmin: false }); const stored = await db.select().from(mcpOauthTokens).where(eq(mcpOauthTokens.grantId, principal.grant.id)); expect(stored.map((t) => t.tokenHash)).toContain(hashMcpSecret(f.tokens.access_token)); expect(JSON.stringify(stored)).not.toContain(f.tokens.access_token); }); it("accepts CIMD native ephemeral ports but binds redemption to the authorized callback", async () => { const f = await fixture(); const clientId = "https://native.example/client.json"; const callback = "http://127.0.0.1:55023/callback"; const cimd = createPublicMcpOAuth(db, config, { metadataFetch: async () => new Response(JSON.stringify({ client_id: clientId, client_name: "Native client", application_type: "native", redirect_uris: ["http://127.0.0.1/callback"], }), { headers: { "content-type": "application/json" } }) }); const request = { client_id: clientId, redirect_uri: callback, response_type: "code", resource: config.resource, code_challenge: challenge, code_challenge_method: "S256" }; await expect(cimd.authorize({ ...request, redirect_uri: "http://127.0.0.1:55023/other" })).rejects.toThrow(); const id = (await cimd.authorize(request)).split("/").at(-1)!; const consent = await cimd.consent(id, f.actor, { decision: "approve", companyId: f.company.id, allowWrites: false }); expect(new URL(consent.redirectUrl).origin).toBe("http://127.0.0.1:55023"); const exchange = { grant_type: "authorization_code", client_id: clientId, redirect_uri: callback, resource: config.resource, code: new URL(consent.redirectUrl).searchParams.get("code"), code_verifier: verifier }; await expect(cimd.token({ ...exchange, redirect_uri: "http://127.0.0.1:55024/callback" })).rejects.toThrow(); expect((await cimd.authenticate((await cimd.token(exchange)).access_token)).grant.companyId).toBe(f.company.id); }); it("pins consent to the requested organization without exposing other memberships", async () => { const f = await fixture(); const [other] = await db.insert(companies).values({ name: "Other organization", issuePrefix: "M" + randomBytes(4).toString("hex") }).returning(); await db.insert(companyMemberships).values({ companyId: other!.id, principalType: "user", principalId: f.actor.userId!, membershipRole: "member", status: "active" }); const input = { client_id: f.client.client_id, redirect_uri: redirectUri, resource: config.resource, response_type: "code", code_challenge: challenge, code_challenge_method: "S256", scope: "paperclip:read paperclip:write", company_id: f.company.id }; const id = (await oauth.authorize(input)).split("/").at(-1)!; // The binding survives a new service instance, and the description reveals only this membership. const resumed = createPublicMcpOAuth(db, config); const [logo] = await db.insert(assets).values({ companyId: f.company.id, provider: "local_disk", objectKey: randomUUID(), contentType: "image/png", byteSize: 1, sha256: "fixture" }).returning(); await db.insert(companyLogos).values({ companyId: f.company.id, assetId: logo!.id }); expect(await resumed.describeRequest(id, f.actor, null)).toMatchObject({ requestedCompanyId: f.company.id, companies: [{ id: f.company.id, logoUrl: `/api/assets/${logo!.id}/content` }] }); expect((await resumed.describeRequest(id, f.actor, null)).companies).toHaveLength(1); expect((await resumed.describeRequest(id, { type: "none" }, null)).companies).toEqual([]); await expect(resumed.consent(id, f.actor, { decision: "approve", companyId: other!.id, allowWrites: true })).rejects.toMatchObject({ status: 403 }); // A concurrent conversation has an independent binding; rejection did not consume either request. const otherId = (await oauth.authorize({ ...input, company_id: other!.id })).split("/").at(-1)!; expect((await resumed.describeRequest(otherId, f.actor, null)).companies).toEqual([{ id: other!.id, name: other!.name, logoUrl: null, canWrite: true }]); const consent = await resumed.consent(id, f.actor, { decision: "approve", companyId: f.company.id, allowWrites: true }); const tokens = await resumed.token({ ...f.exchange, code: new URL(consent.redirectUrl).searchParams.get("code")! }); expect((await resumed.authenticate(tokens.access_token)).grant.companyId).toBe(f.company.id); await expect(resumed.consent(otherId, f.actor, { decision: "approve", companyId: f.company.id, allowWrites: false })).rejects.toMatchObject({ status: 403 }); await resumed.consent(otherId, f.actor, { decision: "deny", allowWrites: false }); // Direct connections retain explicit choice across the user's companies. const directId = (await oauth.authorize({ ...input, company_id: undefined })).split("/").at(-1)!; const direct = await resumed.describeRequest(directId, f.actor, null); expect(direct.requestedCompanyId).toBeNull(); expect(direct.companies.map(c => c.id).sort()).toEqual([f.company.id, other!.id].sort()); await resumed.consent(directId, f.actor, { decision: "approve", companyId: other!.id, allowWrites: false }); await expect(oauth.authorize({ ...input, company_id: "invalid" })).rejects.toThrow(); }); it("does not fall back to another organization when the requested one is unavailable", async () => { const f = await fixture(); const begin = async (companyId: string) => (await oauth.authorize({ client_id: f.client.client_id, redirect_uri: redirectUri, resource: config.resource, response_type: "code", code_challenge: challenge, code_challenge_method: "S256", company_id: companyId })).split("/").at(-1)!; const missing = await begin(randomUUID()); expect((await oauth.describeRequest(missing, f.actor, null)).companies).toEqual([]); await expect(oauth.consent(missing, f.actor, { decision: "approve", companyId: f.company.id, allowWrites: false })).rejects.toMatchObject({ status: 403 }); await oauth.consent(missing, f.actor, { decision: "deny", allowWrites: false }); for (const unavailable of ["membership", "archived"] as const) { const id = await begin(f.company.id); if (unavailable === "membership") await db.update(companyMemberships).set({ status: "inactive" }).where(eq(companyMemberships.id, f.membership.id)); else await db.update(companies).set({ status: "archived" }).where(eq(companies.id, f.company.id)); expect((await oauth.describeRequest(id, f.actor, null)).companies).toEqual([]); await expect(oauth.consent(id, f.actor, { decision: "approve", companyId: f.company.id, allowWrites: false })).rejects.toMatchObject({ status: 403 }); await oauth.consent(id, f.actor, { decision: "deny", allowWrites: false }); await db.update(companyMemberships).set({ status: "active" }).where(eq(companyMemberships.id, f.membership.id)); } }); it("reads the experimental switch live and keeps connection revocation available while disabled", async () => { const f = await fixture(); const revocable = await fixture(); const settings = instanceSettingsService(db); const dispatch = vi.fn(); const execute = createPublicMcpExecutor(db, oauth, dispatch); const app = express(); app.use(express.json()); app.use(publicMcpIngressRoutes(oauth, execute)); app.use((req, _res, next) => { req.actor = f.actor; next(); }); app.use("/api", publicMcpManagementRoutes(oauth)); try { await settings.updateExperimental({ enablePublicMcp: false }); expect(await createPublicMcpOAuth(db, config).isEnabled()).toBe(false); for (const path of ["/.well-known/oauth-authorization-server", "/.well-known/oauth-protected-resource/mcp/paperclip"]) { expect((await request(app).get(path)).status).toBe(503); } expect((await request(app).post("/mcp/paperclip").set("Authorization", `Bearer ${f.tokens.access_token}`).send({})).status).toBe(503); await expect(oauth.register({ client_name: "disabled", redirect_uris: [redirectUri] })).rejects.toMatchObject({ status: 503 }); await expect(oauth.token({ grant_type: "refresh_token", client_id: f.client.client_id, resource: config.resource, refresh_token: f.tokens.refresh_token })).rejects.toMatchObject({ status: 503 }); await expect(execute(f.tokens.access_token, "paperclip_list_agents", { companyId: f.company.id })).rejects.toMatchObject({ status: 503 }); expect(dispatch).not.toHaveBeenCalled(); const setup = await request(app).get("/api/mcp/setup").set("X-Forwarded-Host", "attacker.example"); expect(setup.status).toBe(200); expect(setup.headers["cache-control"]).toBe("no-store"); expect(setup.body).toMatchObject({ enabled: false, serverUrl: config.resource }); expect((await request(app).get("/api/mcp/connections")).status).toBe(200); expect((await request(app).post("/mcp/oauth/revoke").send({ token: revocable.tokens.access_token, client_id: revocable.client.client_id })).status).toBe(200); } finally { await settings.updateExperimental({ enablePublicMcp: true }); } expect((await request(app).get("/api/mcp/setup")).body).toMatchObject({ enabled: true, serverUrl: config.resource }); expect((await request(app).get("/.well-known/oauth-authorization-server")).status).toBe(200); await expect(oauth.authenticate(f.tokens.access_token)).resolves.toMatchObject({ grant: { userId: f.actor.userId } }); await expect(oauth.authenticate(revocable.tokens.access_token)).rejects.toThrow(); }); it("limits assistant setup metadata to signed-in humans", async () => { const app = express(); let actor: Request["actor"] = { type: "none" }; app.use((req, _res, next) => { req.actor = actor; next(); }); app.use("/api", publicMcpManagementRoutes(oauth)); for (const candidate of [ { type: "none" }, { type: "agent", agentId: randomUUID(), companyId: randomUUID() }, { type: "board", source: "local_implicit", userId: "board" }, { type: "board", source: "mcp_oauth", userId: randomUUID() }, ] as Request["actor"][]) { actor = candidate; expect((await request(app).get("/api/mcp/setup")).status).toBe(401); } for (const source of ["session", "cloud_tenant"] as const) { actor = { type: "board", source, userId: randomUUID() }; expect((await request(app).get("/api/mcp/setup")).body).toMatchObject({ enabled: true, serverUrl: config.resource }); } }); it("rotates refresh tokens and persists revocation on replay", async () => { const f = await fixture(); const refresh = { grant_type: "refresh_token", client_id: f.client.client_id, resource: config.resource, refresh_token: f.tokens.refresh_token }; await expect(oauth.token({ ...refresh, resource: "https://other.example" })).rejects.toThrow(); const next = await oauth.token(refresh); expect(next.refresh_token).not.toBe(f.tokens.refresh_token); await expect(oauth.token(refresh)).rejects.toThrow(); await expect(oauth.authenticate(next.access_token)).rejects.toThrow(); }); it.each([new Date(0), null])("rejects personal refresh tokens with invalid expiry %s", async expiresAt => { const f = await fixture(); const [stored] = await db.select().from(mcpOauthTokens).where(eq(mcpOauthTokens.tokenHash, hashMcpSecret(f.tokens.refresh_token!))); expect(stored!.expiresAt!.getTime() - Date.now()).toBeGreaterThan(29 * 24 * 60 * 60_000); await db.update(mcpOauthTokens).set({ expiresAt }).where(eq(mcpOauthTokens.id, stored!.id)); await expect(oauth.token({ grant_type: "refresh_token", client_id: f.client.client_id, resource: config.resource, refresh_token: f.tokens.refresh_token })).rejects.toThrow(); }); it("rejects token expiry, revocation and membership loss on the next call", async () => { const f = await fixture(); await db.update(mcpOauthTokens).set({ expiresAt: new Date(0) }).where(eq(mcpOauthTokens.tokenHash, hashMcpSecret(f.tokens.access_token))); await expect(oauth.authenticate(f.tokens.access_token)).rejects.toThrow(); const g = await fixture(); await db.update(companyMemberships).set({ status: "inactive" }).where(eq(companyMemberships.id, g.membership.id)); await expect(oauth.authenticate(g.tokens.access_token)).rejects.toThrow(); const h = await fixture(); await oauth.revokeToken(h.tokens.access_token, h.client.client_id); await expect(oauth.authenticate(h.tokens.access_token)).rejects.toThrow(); }); it("rejects cross-company calls, missing write scope and changed viewer roles before dispatch", async () => { const f = await fixture("member", false); const dispatch = vi.fn(); const execute = createPublicMcpExecutor(db, oauth, dispatch); await expect(execute(f.tokens.access_token, "paperclip_list_agents", { companyId: randomUUID() })).rejects.toThrow(); const args = { companyId: f.company.id, requestId: randomUUID(), taskId: randomUUID(), body: "feedback" }; await expect(execute(f.tokens.access_token, "paperclip_add_comment", args)).rejects.toThrow(); const g = await fixture(); await db.update(companyMemberships).set({ membershipRole: "viewer" }).where(eq(companyMemberships.id, g.membership.id)); await expect(execute(g.tokens.access_token, "paperclip_add_comment", { ...args, companyId: g.company.id })).rejects.toThrow(); expect(dispatch).not.toHaveBeenCalled(); }); it("does not accept MCP bearer tokens as ordinary board or agent API credentials", async () => { const f = await fixture(); const app = express(); app.use(actorMiddleware(db, { deploymentMode: "authenticated", resolveSession: async () => null })); app.get("/api/actor", (req, res) => res.json(req.actor)); const response = await request(app).get("/api/actor").set("Authorization", `Bearer ${f.tokens.access_token}`); expect(response.status).toBe(401); expect(response.body).not.toHaveProperty("type", "board"); expect(response.body).not.toHaveProperty("type", "agent"); }); it("isolates concurrent calls and preserves normal HTTP actor permissions", async () => { const f = await fixture(); const g = await fixture(); const api = express.Router(); api.use(boardMutationGuard()); api.post("/companies/:companyId/issues", (req, res) => { assertCompanyAccess(req, String(req.params.companyId)); res.json({ userId: req.actor.userId, companyId: req.params.companyId, body: req.body }); }); const dispatch = createMcpApiDispatch(api); const fp = await oauth.authenticate(f.tokens.access_token); const gp = await oauth.authenticate(g.tokens.access_token); const [one, two] = await Promise.all([dispatch(fp, "POST", `/companies/${f.company.id}/issues`, { title: "one" }), dispatch(gp, "POST", `/companies/${g.company.id}/issues`, { title: "two" })]); expect(one).toMatchObject({ userId: f.actor.userId, body: { title: "one" } }); expect(two).toMatchObject({ userId: g.actor.userId, body: { title: "two" } }); const unicodeDescription = "字".repeat(50000); expect(await dispatch(fp, "POST", `/companies/${f.company.id}/issues`, { description: unicodeDescription })).toHaveProperty("body.description", unicodeDescription); await expect(dispatch(fp, "POST", `/companies/${g.company.id}/issues`, {})).rejects.toThrow("403"); }); it("replays completed writes, rejects argument changes and contains concurrent/unknown outcomes", async () => { const f = await fixture(); const dispatch = vi.fn().mockResolvedValue({ id: randomUUID(), title: "durable" }); const execute = createPublicMcpExecutor(db, oauth, dispatch); const args = { companyId: f.company.id, requestId: randomUUID(), title: "durable", description: "A report", assigneeAgentId: randomUUID() }; const first = await execute(f.tokens.access_token, "paperclip_create_task", args); expect(await execute(f.tokens.access_token, "paperclip_create_task", args)).toEqual(first); const reconnectedClient = await oauth.register({ client_name: "Reconnected assistant", redirect_uris: [redirectUri] }); const reconnectId = (await oauth.authorize({ client_id: reconnectedClient.client_id, redirect_uri: redirectUri, resource: config.resource, scope: "paperclip:read paperclip:write", response_type: "code", code_challenge: challenge, code_challenge_method: "S256" })).split("/").at(-1)!; const reconnectConsent = await oauth.consent(reconnectId, f.actor, { decision: "approve", companyId: f.company.id, allowWrites: true }); const reconnected = await oauth.token({ grant_type: "authorization_code", client_id: reconnectedClient.client_id, resource: config.resource, redirect_uri: redirectUri, code: new URL(reconnectConsent.redirectUrl).searchParams.get("code"), code_verifier: verifier }); expect(await execute(reconnected.access_token, "paperclip_create_task", args)).toEqual(first); expect(dispatch).toHaveBeenCalledTimes(1); expect(dispatch.mock.calls[0]?.[3]).toMatchObject({ status: "todo", title: "durable" }); await expect(execute(f.tokens.access_token, "paperclip_create_task", { ...args, title: "changed" })).rejects.toThrow("different arguments"); dispatch.mockRejectedValueOnce(new Error("lost response")); const uncertain = { ...args, requestId: randomUUID() }; expect(await execute(f.tokens.access_token, "paperclip_create_task", uncertain)).toHaveProperty("outcome", "unknown"); expect(await execute(f.tokens.access_token, "paperclip_create_task", uncertain)).toHaveProperty("outcome", "unknown"); expect(dispatch).toHaveBeenCalledTimes(2); const concurrent = { ...args, requestId: randomUUID() }; await Promise.all([execute(f.tokens.access_token, "paperclip_create_task", concurrent), execute(f.tokens.access_token, "paperclip_create_task", concurrent)]); expect(dispatch).toHaveBeenCalledTimes(3); expect(await db.select().from(mcpMutationReceipts).where(eq(mcpMutationReceipts.requestId, concurrent.requestId))).toHaveLength(1); }); it("does not re-elevate an instance administrator beyond company-role permissions", async () => { const f = await fixture("viewer", false); await db.insert(instanceUserRoles).values({ userId: f.actor.userId!, role: "instance_admin" }); const principal = await oauth.authenticate(f.tokens.access_token); const decision = await authorizationService(db).decide({ actor: principal.actor, action: "tasks:assign", resource: { type: "company", companyId: f.company.id } }); expect(decision.allowed).toBe(false); }); it("uses real task/document routes, creates one durable task, and attributes feedback to the person", async () => { const f = await fixture(); // Capture the existing scheduling boundary without launching a paid/local agent. const wake = vi.spyOn(assignmentWakeups, "queueIssueAssignmentWakeup").mockResolvedValue(null); try { const [agent] = await db.insert(agents).values({ companyId: f.company.id, name: "Researcher", role: "researcher", adapterType: "process", status: "idle" }).returning(); const api = express.Router(); api.use(boardMutationGuard()); api.use(issueRoutes(db, createStorageService(createLocalDiskStorageProvider("/tmp/paperclip-public-mcp-unused-storage")))); api.use(activityRoutes(db)); const execute = createPublicMcpExecutor(db, oauth, createMcpApiDispatch(api)); const args = { companyId: f.company.id, requestId: randomUUID(), title: "Write a durable report", description: "Research the supplied material", assigneeAgentId: agent!.id }; const result = await execute(f.tokens.access_token, "paperclip_create_task", args); expect(result).toHaveProperty("task"); const task = result.task as { id: string; status: string }; expect(task.status).toBe("todo"); expect(await execute(f.tokens.access_token, "paperclip_create_task", args)).toEqual(result); expect(await db.select().from(issues).where(eq(issues.companyId, f.company.id))).toHaveLength(1); expect(wake).toHaveBeenCalledTimes(1); expect(wake.mock.calls[0]?.[0]).toMatchObject({ requestedByActorType: "user", requestedByActorId: f.actor.userId }); // Avoid starting execution when adding feedback; the existing route accepts a completed task without reopening it. await db.update(issues).set({ status: "done", assigneeAgentId: null }).where(eq(issues.id, task.id)); const commentArgs = { companyId: f.company.id, taskId: task.id, requestId: randomUUID(), body: "Please include the source citations." }; const feedback = await execute(f.tokens.access_token, "paperclip_add_comment", commentArgs); expect(feedback).toHaveProperty("comment.authorUserId", f.actor.userId); expect(await execute(f.tokens.access_token, "paperclip_add_comment", commentArgs)).toEqual(feedback); expect(await db.select().from(issueComments).where(eq(issueComments.issueId, task.id))).toHaveLength(1); await documentService(db).upsertIssueDocument({ issueId: task.id, key: "report", title: "Research report", format: "markdown", body: "A concrete durable result.", createdByUserId: f.actor.userId }); // A fresh executor represents another assistant conversation; no mutable company/session state is shared. const later = createPublicMcpExecutor(db, oauth, createMcpApiDispatch(api)); expect(await later(f.tokens.access_token, "paperclip_read_document", { companyId: f.company.id, taskId: task.id, key: "report" })).toHaveProperty("document.body", "A concrete durable result."); const g = await fixture(); await expect(later(g.tokens.access_token, "paperclip_read_document", { companyId: g.company.id, taskId: task.id, key: "report" })).rejects.toThrow(); const unavailable = { ...args, requestId: randomUUID(), assigneeAgentId: randomUUID() }; const rejected = await execute(f.tokens.access_token, "paperclip_create_task", unavailable); expect(rejected).toHaveProperty("outcome", "rejected"); expect(await execute(f.tokens.access_token, "paperclip_create_task", unavailable)).toEqual(rejected); expect(wake).toHaveBeenCalledTimes(1); // Human assignment to a paused agent is deliberate and allowed by existing routes. // Creating its queued task must neither unpause the agent nor claim execution started. await db.update(agents).set({ status: "paused" }).where(eq(agents.id, agent!.id)); const paused = await execute(f.tokens.access_token, "paperclip_create_task", { ...args, requestId: randomUUID(), title: "Wait for the paused researcher" }); expect(paused).toHaveProperty("task.status", "todo"); expect(paused.scheduling).toContain("does not guarantee execution has started"); expect((await db.select().from(agents).where(eq(agents.id, agent!.id)))[0]?.status).toBe("paused"); } finally { wake.mockRestore(); } }); it("requires fresh explicit configuration consent and never upscopes old grants", async () => { const old = await fixture(); const f = await fixture("member", true, true); const dispatch = vi.fn().mockResolvedValue({ id: randomUUID(), name: "Updated" }); const execute = createPublicMcpExecutor(db, oauth, dispatch); const args = { companyId: old.company.id, agentId: randomUUID(), requestId: randomUUID(), changes: { title: "Writer" } }; await expect(execute(old.tokens.access_token, "paperclip_update_agent", args)).rejects.toThrow("paperclip:configure"); expect(dispatch).not.toHaveBeenCalled(); await execute(f.tokens.access_token, "paperclip_update_agent", { ...args, companyId: f.company.id }); expect(dispatch).toHaveBeenCalledTimes(1); expect((await oauth.authenticate(f.tokens.access_token)).grant.scopes).toContain("paperclip:configure"); await db.update(companyMemberships).set({ membershipRole: "viewer" }).where(eq(companyMemberships.id, f.membership.id)); await expect(execute(f.tokens.access_token, "paperclip_update_agent", { ...args, companyId: f.company.id, requestId: randomUUID() })).rejects.toThrow("permission"); const refreshed = await oauth.token({ grant_type: "refresh_token", client_id: old.client.client_id, resource: config.resource, refresh_token: old.tokens.refresh_token }); expect(refreshed.scope).not.toContain("configure"); }); it("shares named/API receipts and refuses unknown operations, fields and cross-company arguments", async () => { const f = await fixture(); const dispatch = vi.fn().mockResolvedValue({ id: randomUUID(), title: "New title" }); const execute = createPublicMcpExecutor(db, oauth, dispatch); const args = { companyId: f.company.id, taskId: randomUUID(), requestId: randomUUID(), changes: { title: "New title" } }; const first = await execute(f.tokens.access_token, "paperclip_update_task", args); expect(await execute(f.tokens.access_token, "paperclip_call_api", { companyId: f.company.id, operationId: "paperclip_update_task", arguments: args })).toEqual(first); expect(dispatch).toHaveBeenCalledTimes(1); for (const field of ["onBehalfOfUserId", "reviewPolicy", "executionPolicy", "interrupt", "reviewInteractionId", "permissions"]) { await expect(execute(f.tokens.access_token, "paperclip_update_task", { ...args, changes: { [field]: "unauthorized" } })).rejects.toThrow(); } for (const operationId of ["DELETE /companies/:id", "paperclip_call_api", "approve_approval", "https://evil.example"]) { await expect(execute(f.tokens.access_token, "paperclip_call_api", { companyId: f.company.id, operationId, arguments: args })).rejects.toThrow(); } await expect(execute(f.tokens.access_token, "paperclip_call_api", { companyId: f.company.id, operationId: "paperclip_update_task", arguments: { ...args, companyId: randomUUID() } })).rejects.toThrow("Company"); expect(dispatch).toHaveBeenCalledTimes(1); }); async function expandedFixture(configure = false) { const f = await fixture(configure ? "owner" : "member", true, configure); if (configure) await db.insert(principalPermissionGrants).values(["agents:configure", "skills:create"].map(permissionKey => ({ companyId: f.company.id, principalType: "user", principalId: f.actor.userId!, permissionKey }))); const storageDir = await mkdtemp(join(tmpdir(), "mcp-files-")); const storage = createStorageService(createLocalDiskStorageProvider(storageDir)); const api = express.Router(); api.use(boardMutationGuard()); api.use(issueRoutes(db, storage)); api.use(activityRoutes(db)); api.use(projectRoutes(db)); api.use(agentRoutes(db)); api.use(companySkillRoutes(db)); const dispatch = createMcpApiDispatch(api); const transfers = createPublicMcpTransfers(db, oauth, dispatch, storage); const execute = createPublicMcpExecutor(db, oauth, dispatch, transfers); const [issue] = await db.insert(issues).values({ companyId: f.company.id, title: "Work", status: "backlog" }).returning(); const call = (name: string, a: Record = {}) => execute(f.tokens.access_token, name, { companyId: f.company.id, ...a }); return { ...f, issue: issue!, call, dispatch, transfers, storage, cleanup: () => rm(storageDir, { recursive: true, force: true }) }; } async function makePrivate(f: Awaited>) { const owner = randomUUID(); await db.insert(authUsers).values({ id: owner, name: "Private owner", email: owner + "@example.com", createdAt: new Date(), updatedAt: new Date() }); await db.insert(companyMemberships).values({ companyId: f.company.id, principalType: "user", principalId: owner, membershipRole: "member", status: "active" }); await db.update(issues).set({ visibility: "private", privacyRootIssueId: f.issue.id, responsibleUserId: owner, createdByUserId: owner }).where(eq(issues.id, f.issue.id)); return owner; } it("hides private tasks from assistant search, reads, edits and file-link creation", async () => { const f = await expandedFixture(); try { await makePrivate(f); expect(await f.call("paperclip_search_tasks")).toMatchObject({ tasks: [] }); for (const name of ["paperclip_read_task", "paperclip_list_deliverables", "paperclip_list_document_revisions"]) { await expect(f.call(name, { taskId: f.issue.id, ...(name === "paperclip_list_document_revisions" ? { key: "plan" } : {}) })).rejects.toMatchObject({ status: 404 }); } expect(await f.call("paperclip_update_task", { taskId: f.issue.id, requestId: randomUUID(), changes: { title: "Unauthorized edit" } })).toMatchObject({ outcome: "rejected", status: 404 }); await expect(f.call("paperclip_get_upload_url", { taskId: f.issue.id, requestId: randomUUID(), filename: "data.txt", contentType: "text/plain", byteSize: 2, sha256: createHash("sha256").update("hi").digest("hex") })).rejects.toThrow(); expect((await db.select().from(issues).where(eq(issues.id, f.issue.id)))[0]?.title).toBe("Work"); expect(await db.select().from(mcpAttachmentUploads).where(eq(mcpAttachmentUploads.taskId, f.issue.id))).toHaveLength(0); } finally { await f.cleanup(); } }); it("revokes existing assistant upload and download links when private task sharing is removed", async () => { const f = await expandedFixture(); try { const owner = await makePrivate(f); const [grant] = await db.insert(issueAccessGrants).values({ issueId: f.issue.id, subjectType: "user", subjectId: f.actor.userId!, source: "explicit", grantedByUserId: owner }).returning(); const bytes = Buffer.from("Private attachment"); const args = { taskId: f.issue.id, filename: "private.txt", contentType: "text/plain", byteSize: bytes.length, sha256: createHash("sha256").update(bytes).digest("hex") }; const app = express(); app.use(f.transfers.router); const uploadedUrl = new URL((await f.call("paperclip_get_upload_url", { ...args, requestId: randomUUID() })).url as string); const uploaded = await request(app).put(uploadedUrl.pathname + uploadedUrl.search).set("Content-Type", args.contentType).send(bytes); expect(uploaded.status).toBe(200); const download = new URL((await f.call("paperclip_get_download_url", { attachmentId: uploaded.body.attachment.id })).url as string); const pending = await f.call("paperclip_get_upload_url", { ...args, requestId: randomUUID() }); const pendingUrl = new URL(pending.url as string); expect((await request(app).get(download.pathname + download.search)).status).toBe(200); await db.update(issueAccessGrants).set({ revokedAt: new Date() }).where(eq(issueAccessGrants.id, grant!.id)); expect((await request(app).get(download.pathname + download.search)).status).not.toBe(200); expect((await request(app).put(pendingUrl.pathname + pendingUrl.search).set("Content-Type", args.contentType).send(bytes)).status).toBe(403); expect((await db.select().from(mcpAttachmentUploads).where(eq(mcpAttachmentUploads.id, pending.uploadId as string)))[0]?.attachmentId).toBeNull(); await expect(f.call("paperclip_get_download_url", { attachmentId: uploaded.body.attachment.id })).rejects.toThrow(); } finally { await f.cleanup(); } }); it("edits, blocks and completes tasks through real routes and rejects review overrides", async () => { const f = await expandedFixture(); try { expect(await f.call("paperclip_update_task", { taskId: f.issue.id, requestId: randomUUID(), changes: { title: "Edited", description: "Durable detail" } })).toHaveProperty("task.title", "Edited"); expect(await f.call("paperclip_block_task", { taskId: f.issue.id, requestId: randomUUID(), unblockDescriptor: { owner: "board", action: "Supply the source file" } })).toHaveProperty("task.status", "blocked"); expect(await f.call("paperclip_finish_task", { taskId: f.issue.id, requestId: randomUUID() })).toHaveProperty("task.status", "done"); await db.update(issues).set({ status: "in_review" }).where(eq(issues.id, f.issue.id)); const denied = await f.call("paperclip_finish_task", { taskId: f.issue.id, requestId: randomUUID() }); expect(denied).toMatchObject({ outcome: "rejected", status: 403, code: "MCP_REVIEW_REQUIRED" }); expect((await db.select().from(issues).where(eq(issues.id, f.issue.id)))[0]?.status).toBe("in_review"); const logs = await db.select().from(activityLog).where(eq(activityLog.entityId, f.issue.id)); expect(logs.some(v => v.actorType === "user" && v.actorId === f.actor.userId && v.action === "issue.updated")).toBe(true); } finally { await f.cleanup(); } }); it.each(["queued", "running", "scheduled_retry"])("preserves %s execution ownership and rejects dependency cycles", async status => { const f = await expandedFixture(); try { const [agent] = await db.insert(agents).values({ companyId: f.company.id, name: "Worker" }).returning(); const [run] = await db.insert(heartbeatRuns).values({ companyId: f.company.id, agentId: agent!.id, status, runtimeMode: "native", nativeIssueId: f.issue.id }).returning(); await db.update(issues).set({ assigneeAgentId: agent!.id, executionRunId: run!.id, status: "in_progress" }).where(eq(issues.id, f.issue.id)); for (const changes of [{ status: "done" }, { status: "blocked", unblockDescriptor: { owner: "board", action: "Supply data" } }, { assigneeAgentId: null }, { status: "cancelled" }]) { expect(await f.call("paperclip_update_task", { taskId: f.issue.id, requestId: randomUUID(), changes })).toMatchObject({ outcome: "rejected", status: 409, code: "MCP_ACTIVE_EXECUTION" }); } expect((await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, run!.id)))[0]?.status).toBe(status); expect(await f.call("paperclip_update_task", { taskId: f.issue.id, requestId: randomUUID(), changes: { title: "Clarified title" } })).toHaveProperty("task.title", "Clarified title"); await db.update(heartbeatRuns).set({ status: "succeeded" }).where(eq(heartbeatRuns.id, run!.id)); await db.update(issues).set({ assigneeAgentId: null, executionRunId: null, status: "backlog" }).where(eq(issues.id, f.issue.id)); const [second] = await db.insert(issues).values({ companyId: f.company.id, title: "Dependent", status: "backlog" }).returning(); expect(await f.call("paperclip_update_task", { taskId: second!.id, requestId: randomUUID(), changes: { blockedByIssueIds: [f.issue.id] } })).toHaveProperty("task.id", second!.id); const cycle = await f.call("paperclip_update_task", { taskId: f.issue.id, requestId: randomUUID(), changes: { blockedByIssueIds: [second!.id] } }); expect(cycle).toMatchObject({ outcome: "rejected" }); expect([400, 409, 422]).toContain(cycle.status); } finally { await f.cleanup(); } }); it.each([ ["codex_local", "gpt-5.4", "modelReasoningEffort"], ["claude_local", "claude-sonnet-4-6", "effort"], ["paperclip_runner", "gpt-5.4", "modelReasoningEffort"], ])("updates the effective %s reasoning setting and validates its model", async (adapterType, model, field) => { const f = await expandedFixture(true); try { const [agent] = await db.insert(agents).values({ companyId: f.company.id, name: "Reasoning", adapterType, adapterConfig: { model, [field]: "high", instructionsBundleMode: "managed" }, runtimeConfig: { heartbeat: { enabled: false } } }).returning(); const edited = await f.call("paperclip_update_agent", { agentId: agent!.id, requestId: randomUUID(), changes: { adapterConfig: { reasoningEffort: "low" } } }); expect(edited, JSON.stringify(edited)).toHaveProperty("agent.adapterConfig.reasoningEffort", "low"); const stored = (await db.select().from(agents).where(eq(agents.id, agent!.id)))[0]!; expect(stored.adapterConfig[field]).toBe("low"); expect(stored.adapterConfig.reasoningEffort).toBeUndefined(); expect(await f.call("paperclip_get_agent", { agentId: agent!.id })).toHaveProperty("agent.adapterConfig.reasoningEffort", "low"); for (const reasoningEffort of ["ultra"]) { expect(await f.call("paperclip_update_agent", { agentId: agent!.id, requestId: randomUUID(), changes: { adapterConfig: { reasoningEffort } } })).toMatchObject({ outcome: "rejected" }); } if (adapterType === "claude_local") { expect(await f.call("paperclip_update_agent", { agentId: agent!.id, requestId: randomUUID(), changes: { adapterConfig: { model: "claude-haiku-4-5" } } })).toMatchObject({ outcome: "rejected", status: 422 }); } } finally { await f.cleanup(); } }); it("writes durable documents and rejects stale revisions through both interfaces", async () => { const f = await expandedFixture(); try { const a = { taskId: f.issue.id, requestId: randomUUID(), key: "report", document: { format: "markdown", body: "First revision", baseRevisionId: null } }; const first = await f.call("paperclip_write_document", a); expect(first).toHaveProperty("document.body", "First revision"); const revision = (first.document as { latestRevisionId: string }).latestRevisionId; expect(revision).toBeTruthy(); const second = await f.call("paperclip_write_document", { ...a, requestId: randomUUID(), document: { ...a.document, body: "Second revision", baseRevisionId: revision } }); expect(second).toHaveProperty("document.body", "Second revision"); expect(await f.call("paperclip_write_document", { ...a, requestId: randomUUID() })).toMatchObject({ outcome: "rejected", status: 409 }); expect(await f.call("paperclip_read_document", { taskId: f.issue.id, key: "report" })).toHaveProperty("document.body", "Second revision"); expect(await f.call("paperclip_list_document_revisions", { taskId: f.issue.id, key: "report" })).toHaveProperty("revisions"); } finally { await f.cleanup(); } }); it("creates and edits projects and agents while rejecting privileged configuration fields", async () => { const f = await expandedFixture(true); try { const created = await f.call("paperclip_create_project", { requestId: randomUUID(), project: { name: "Research", description: "Project context" } }); expect(created).toHaveProperty("project.name", "Research"); const id = (created.project as { id: string }).id; expect(await f.call("paperclip_update_project", { projectId: id, requestId: randomUUID(), changes: { name: "Updated project" } })).toHaveProperty("project.name", "Updated project"); const [agent] = await db.insert(agents).values({ companyId: f.company.id, name: "Writer", adapterType: "codex_local", adapterConfig: { instructionsBundleMode: "managed", model: "gpt-5.4-mini", env: { SAFE_EXISTING: "retain" } }, runtimeConfig: { heartbeat: { enabled: false } } }).returning(); const edited = await f.call("paperclip_update_agent", { agentId: agent!.id, requestId: randomUUID(), changes: { title: "Editor", budgetMonthlyCents: 1200, adapterConfig: { model: "gpt-5.4" } } }); expect(edited, JSON.stringify(edited)).toHaveProperty("agent.title", "Editor"); const stored = (await db.select().from(agents).where(eq(agents.id, agent!.id)))[0]!; expect(stored.adapterConfig.env).toEqual({ SAFE_EXISTING: { type: "plain", value: "retain" } }); expect(stored.budgetMonthlyCents).toBe(1200); const read = await f.call("paperclip_get_agent", { agentId: agent!.id }); expect(JSON.stringify(read)).not.toContain("SAFE_EXISTING"); for (const changes of [{ permissions: {} }, { adapterConfig: { command: "evil" } }, { runtimeConfig: { debug: { providerTrace: "raw" } } }]) { await expect(f.call("paperclip_update_agent", { agentId: agent!.id, requestId: randomUUID(), changes })).rejects.toThrow(); } } finally { await f.cleanup(); } }); it("rejects unchecked legacy prompt edits through named and generic operations", async () => { const f = await expandedFixture(true); try { const [agent] = await db.insert(agents).values({ companyId: f.company.id, name: "Legacy writer", adapterType: "codex_local", adapterConfig: { instructionsBundleMode: "managed", promptTemplate: "Newer human instructions" } }).returning(); for (const base of [{ baseHash: null }, { baseHash: "0".repeat(64) }, { baseRevisionId: randomUUID() }]) { const args = { companyId: f.company.id, agentId: agent!.id, requestId: randomUUID(), file: { path: "promptTemplate.legacy.md", content: "Stale assistant overwrite", ...base } }; expect(await f.call("paperclip_update_agent_instructions", args)).toMatchObject({ outcome: "rejected", status: 422, code: "MCP_LEGACY_INSTRUCTIONS_UNVERSIONED" }); expect(await f.call("paperclip_call_api", { operationId: "paperclip_update_agent_instructions", arguments: { ...args, requestId: randomUUID() } })).toMatchObject({ outcome: "rejected", status: 422, code: "MCP_LEGACY_INSTRUCTIONS_UNVERSIONED" }); } const stored = (await db.select().from(agents).where(eq(agents.id, agent!.id)))[0]!; expect(stored.adapterConfig.promptTemplate).toBe("Newer human instructions"); } finally { await f.cleanup(); } }); it("version-checks agent instructions and skill files through the real domain services", async () => { const f = await expandedFixture(true); try { const [agent] = await db.insert(agents).values({ companyId: f.company.id, name: "Writer", adapterType: "codex_local", adapterConfig: { instructionsBundleMode: "managed" } }).returning(); const input = { agentId: agent!.id, requestId: randomUUID(), file: { path: "AGENTS.md", content: "First instructions", baseRevisionId: null } }; expect(await f.call("paperclip_update_agent_instructions", input)).toHaveProperty("instructions.content", "First instructions"); const current = await f.call("paperclip_read_agent_instructions", { agentId: agent!.id }); const instructions = current.instructions as { revision: { id: string } }; expect(instructions.revision.id).toBeTruthy(); expect(await f.call("paperclip_update_agent_instructions", { ...input, requestId: randomUUID(), file: { ...input.file, content: "New instructions", baseRevisionId: instructions.revision.id } })).toHaveProperty("instructions.content", "New instructions"); expect(await f.call("paperclip_update_agent_instructions", { ...input, requestId: randomUUID() })).toMatchObject({ outcome: "rejected", status: 409 }); expect(await f.call("paperclip_list_agent_instruction_revisions", { agentId: agent!.id })).toHaveProperty("revisions"); const created = await f.call("paperclip_create_skill", { requestId: randomUUID(), skill: { name: "Source review", markdown: "# Source review\nReview citations carefully." } }); expect(created).toHaveProperty("skill.id"); const skill = created.skill as { id: string; currentVersionId: string }; expect(skill.currentVersionId).toBeTruthy(); const read = await f.call("paperclip_read_skill_file", { skillId: skill.id }); const file = read.file as { content: string }; const write = { skillId: skill.id, requestId: randomUUID(), file: { path: "SKILL.md", content: file.content + "\nPreserve source links.", expectedVersionId: skill.currentVersionId } }; expect(await f.call("paperclip_write_skill_file", write)).toHaveProperty("file.versionId"); expect(await f.call("paperclip_write_skill_file", { ...write, requestId: randomUUID() })).toMatchObject({ outcome: "rejected", status: 409 }); expect(await f.call("paperclip_update_skill", { skillId: skill.id, requestId: randomUUID(), changes: { tagline: "Check sources" } })).toHaveProperty("skill.tagline", "Check sources"); } finally { await f.cleanup(); } }); it("completes OAuth and concurrent upload retries with a one-connection pool", async () => { const originalDb = db, originalOauth = oauth; db = createDb(externalUrl ?? temp.connectionString, { maxConnections: 1 }); oauth = createPublicMcpOAuth(db, config); let cleanup: (() => Promise) | undefined; try { // Fixture approval and PKCE redemption must also stay on their transaction. const f = await expandedFixture(); cleanup = f.cleanup; await oauth.token({ grant_type: "refresh_token", client_id: f.client.client_id, resource: config.resource, refresh_token: f.tokens.refresh_token }); const device = await deviceFixture(); await oauth.consentDevice(device.codes.user_code, device.actor, { decision: "approve", companyId: device.company.id, allowWrites: true }); await oauth.token(device.deviceExchange); const bytes = Buffer.from("one connection upload"); const args = { taskId: f.issue.id, filename: "single.txt", contentType: "text/plain", byteSize: bytes.length, sha256: createHash("sha256").update(bytes).digest("hex") }; const links = []; for (let i = 0; i < 3; i++) links.push(new URL((await f.call("paperclip_get_upload_url", { ...args, requestId: randomUUID() })).url as string)); const app = express(); app.use(f.transfers.router); const results = await Promise.all(links.flatMap(url => Array.from({ length: 2 }, () => request(app).put(url.pathname + url.search).set("Content-Type", args.contentType).send(bytes).timeout(5000)))); expect(results.map(result => result.status)).toEqual(Array(6).fill(200)); expect(new Set(results.map(result => result.body.attachment.id)).size).toBe(3); expect((await f.call("paperclip_list_deliverables", { taskId: f.issue.id })).attachments).toHaveLength(3); } finally { db = originalDb; oauth = originalOauth; await cleanup?.(); } }, 15000); it("automatically finalizes binary uploads once, survives lost responses, and enforces live revocation", async () => { const f = await expandedFixture(); try { const bytes = Buffer.from([0, 255, 18, 7, 64, 89, 33, 0]); const sha256 = createHash("sha256").update(bytes).digest("hex"); const args = { taskId: f.issue.id, requestId: randomUUID(), filename: "demo.mp4", contentType: "video/mp4", byteSize: bytes.length, sha256 }; const prepared = await f.call("paperclip_get_upload_url", args); const url = new URL(prepared.url as string); const app = express(); app.use(f.transfers.router); const put = (body = bytes) => request(app).put(url.pathname + url.search).set("Content-Type", args.contentType).send(body); const responses = await Promise.all([put(), put()]); expect(responses.map(v => v.status)).toEqual([200, 200]); const attachment = responses[0]!.body.attachment; expect(responses[1]!.body.attachment.id).toBe(attachment.id); expect(await f.call("paperclip_get_upload_url", args)).toMatchObject({ status: "completed", attachment: { id: attachment.id, sha256 } }); const outputs = await f.call("paperclip_list_deliverables", { taskId: f.issue.id }); expect(outputs.attachments).toHaveLength(1); expect((await put(Buffer.from("conflicting bytes"))).status).toBe(403); const download = await f.call("paperclip_get_download_url", { attachmentId: attachment.id }); const downloadUrl = new URL(download.url as string); const content = await request(app).get(downloadUrl.pathname + downloadUrl.search).buffer(true); expect(content.status).toBe(200); expect(createHash("sha256").update(content.body as Buffer).digest("hex")).toBe(sha256); const tickets = await db.select().from(mcpFileTickets).where(eq(mcpFileTickets.grantId, (await oauth.authenticate(f.tokens.access_token)).grant.id)); expect(JSON.stringify(tickets)).not.toContain(url.searchParams.get("ticket")); const receipts = await db.select().from(mcpMutationReceipts).where(eq(mcpMutationReceipts.companyId, f.company.id)); expect(JSON.stringify(receipts)).not.toContain("ticket="); const other = await fixture(); await expect(createPublicMcpExecutor(db, oauth, f.dispatch, f.transfers)(other.tokens.access_token, "paperclip_get_download_url", { companyId: other.company.id, attachmentId: attachment.id })).rejects.toThrow(); await oauth.revokeConnection((await oauth.authenticate(f.tokens.access_token)).grant.id, f.actor.userId!); expect((await request(app).get(downloadUrl.pathname + downloadUrl.search)).status).not.toBe(200); expect((await put()).status).not.toBe(200); } finally { await f.cleanup(); } }); it("rejects expired, malformed and over-limit file tickets and respects the experimental gate", async () => { const f = await expandedFixture(); try { const args = { taskId: f.issue.id, requestId: randomUUID(), filename: "data.json", contentType: "application/json", byteSize: 2, sha256: createHash("sha256").update("{}").digest("hex") }; await expect(f.call("paperclip_get_upload_url", { ...args, byteSize: MAX_ATTACHMENT_BYTES + 1 })).rejects.toThrow(); await expect(f.call("paperclip_get_upload_url", { ...args, filename: "../escape" })).rejects.toThrow(); const prepared = await f.call("paperclip_get_upload_url", args); const url = new URL(prepared.url as string); const app = express(); app.use(f.transfers.router); const put = () => request(app).put(url.pathname + url.search).set("Content-Type", "application/json").send("{}"); await instanceSettingsService(db).updateExperimental({ enablePublicMcp: false }); try { expect((await put()).status).not.toBe(200); } finally { await instanceSettingsService(db).updateExperimental({ enablePublicMcp: true }); } await db.update(mcpFileTickets).set({ expiresAt: new Date(0) }).where(eq(mcpFileTickets.tokenHash, hashMcpSecret(url.searchParams.get("ticket")!))); expect((await put()).status).toBe(403); expect((await request(app).get("/mcp/files/download?ticket=bad")).status).toBe(403); } finally { await f.cleanup(); } }); it("rejects partial, oversized and mismatched uploads without creating attachments", async () => { const f = await expandedFixture(); try { const bytes = Buffer.from("exact original content"); const args = { taskId: f.issue.id, requestId: randomUUID(), filename: "data.txt", contentType: "text/plain", byteSize: bytes.length, sha256: createHash("sha256").update(bytes).digest("hex") }; const url = new URL((await f.call("paperclip_get_upload_url", args)).url as string); const app = express(); app.use(f.transfers.router); const put = (body: Buffer, type = "text/plain") => request(app).put(url.pathname + url.search).set("Content-Type", type).send(body); expect((await put(bytes.subarray(0, 4))).status).toBe(403); expect((await put(Buffer.alloc(MAX_ATTACHMENT_BYTES + 1))).status).toBe(413); expect((await put(bytes, "video/mp4")).status).toBe(403); expect((await f.call("paperclip_list_deliverables", { taskId: f.issue.id })).attachments).toHaveLength(0); expect((await put(bytes)).status).toBe(200); expect((await f.call("paperclip_list_deliverables", { taskId: f.issue.id })).attachments).toHaveLength(1); } finally { await f.cleanup(); } }); it.each(["unexpected_storage", "oauth_shaped_storage"])("preserves retryable upload storage failures without confusing them with session authority (%s)", async kind => { const f = await expandedFixture(); let restoreStorage = () => {}; try { const bytes = Buffer.from("synthetic storage failure"); const args = { taskId: f.issue.id, requestId: randomUUID(), filename: "data.txt", contentType: "text/plain", byteSize: bytes.length, sha256: createHash("sha256").update(bytes).digest("hex") }; const prepared = await f.call("paperclip_get_upload_url", args); const failure = kind === "oauth_shaped_storage" ? new McpOAuthError("access_denied", "SYNTHETIC_STORAGE_FAILURE_CANARY", 409) : new Error("SYNTHETIC_STORAGE_FAILURE_CANARY"); const putSpy = vi.spyOn(f.storage, "putFile").mockRejectedValueOnce(failure); restoreStorage = () => { putSpy.mockRestore(); }; const app = express(); app.use(f.transfers.router); const url = new URL(prepared.url as string); const response = await request(app).put(url.pathname + url.search).set("Content-Type", "text/plain").send(bytes); expect(putSpy).toHaveBeenCalledTimes(1); expect(response.status).toBe(503); expect(response.body).toEqual({ error: "transfer_unavailable", message: "Transfer could not be confirmed. Retry the same upload identity, or request another download link." }); expect(JSON.stringify(response.body)).not.toContain("SYNTHETIC_STORAGE_FAILURE_CANARY"); const [upload] = await db.select().from(mcpAttachmentUploads).where(eq(mcpAttachmentUploads.id, prepared.uploadId as string)); expect(upload!.attachmentId).toBeNull(); expect((await f.call("paperclip_list_deliverables", { taskId: f.issue.id })).attachments).toHaveLength(0); expect(await db.select().from(activityLog).where(and(eq(activityLog.companyId, f.company.id), eq(activityLog.action, "issue.attachment_added")))).toHaveLength(0); } finally { restoreStorage(); await f.cleanup(); } }); it("rejects authority lost during storage and safely reclaims the orphaned object", async () => { const f = await expandedFixture(); try { const bytes = Buffer.from("orphan test"); const args = { taskId: f.issue.id, requestId: randomUUID(), filename: "data.txt", contentType: "text/plain", byteSize: bytes.length, sha256: createHash("sha256").update(bytes).digest("hex") }; const prepared = await f.call("paperclip_get_upload_url", args); const principal = await oauth.authenticate(f.tokens.access_token); const originalPut = f.storage.putFile.bind(f.storage); const putSpy = vi.spyOn(f.storage, "putFile").mockImplementationOnce(async input => { const stored = await originalPut(input); await oauth.revokeConnection(principal.grant.id, f.actor.userId!); return stored; }); const app = express(); app.use(f.transfers.router); const url = new URL(prepared.url as string); expect((await request(app).put(url.pathname + url.search).set("Content-Type", "text/plain").send(bytes)).status).not.toBe(200); putSpy.mockRestore(); const [upload] = await db.select().from(mcpAttachmentUploads).where(eq(mcpAttachmentUploads.id, prepared.uploadId as string)); expect(upload!.attachmentId).toBeNull(); const orphan = await f.storage.getObject(upload!.companyId, upload!.objectKey); orphan.stream.destroy(); await db.update(mcpAttachmentUploads).set({ expiresAt: new Date(0) }).where(eq(mcpAttachmentUploads.id, upload!.id)); const fresh = await expandedFixture(); try { const clean = createPublicMcpTransfers(db, oauth, fresh.dispatch, f.storage); await clean.getUploadUrl(await oauth.authenticate(fresh.tokens.access_token), { ...args, companyId: fresh.company.id, taskId: fresh.issue.id, requestId: randomUUID() }); expect((await db.select().from(mcpAttachmentUploads).where(eq(mcpAttachmentUploads.id, upload!.id)))[0]!.cleanedAt).not.toBeNull(); await expect(f.storage.getObject(upload!.companyId, upload!.objectKey)).rejects.toThrow(); } finally { await fresh.cleanup(); } } finally { await f.cleanup(); } }); it("exposes discovery and individually named protocol tools, with CSRF-protected management", async () => { const f = await fixture(); const dispatch = vi.fn().mockResolvedValue([{ id: randomUUID(), name: "Engineer", adapterConfig: { secret: "hidden" } }]); const app = express(); app.use(express.json()); app.use(publicMcpIngressRoutes(oauth, createPublicMcpExecutor(db, oauth, dispatch))); app.use((req, _res, next) => { req.actor = f.actor; next(); }); app.use("/api", publicMcpManagementRoutes(oauth)); const discovery = await request(app).get("/.well-known/oauth-authorization-server"); expect(discovery.body.code_challenge_methods_supported).toEqual(["S256"]); expect((await request(app).post("/mcp/paperclip").send({})).status).toBe(401); const rpc = (method: string, params?: unknown) => request(app).post("/mcp/paperclip").timeout({ response: 5000, deadline: 7000 }).set("Authorization", `Bearer ${f.tokens.access_token}`).set("Accept", "application/json, text/event-stream").send({ jsonrpc: "2.0", id: 1, method, ...(params ? { params } : {}) }); expect((await rpc("initialize", { protocolVersion: "2025-03-26", capabilities: {}, clientInfo: { name: "test", version: "1" } })).body.result.serverInfo.name).toBe("paperclip"); const catalog = await rpc("tools/list"); expect(catalog.body.result.tools).toHaveLength(publicMcpCapabilities.length); expect(catalog.body.result.tools.find((t: { name: string }) => t.name === "paperclip_create_task").annotations.readOnlyHint).toBe(false); const result = await rpc("tools/call", { name: "paperclip_list_agents", arguments: { companyId: f.company.id } }); expect(JSON.stringify(result.body)).not.toContain("hidden"); expect(result.body.result.structuredContent.agents[0].name).toBe("Engineer"); dispatch.mockRejectedValueOnce(new Error("Internal database details: tenant-secret")); const failed = await rpc("tools/call", { name: "paperclip_list_agents", arguments: { companyId: f.company.id } }); expect(failed.body.result.isError).toBe(true); expect(JSON.stringify(failed.body)).not.toContain("tenant-secret"); expect(failed.body.result.content[0].text).toContain("could not confirm"); dispatch.mockClear(); const invalid = await rpc("tools/call", { name: "paperclip_finish_task", arguments: { companyId: f.company.id, taskId: randomUUID(), requestId: "b2c3d4e5-f6a7-48b9-c0d1-e2f3a4b5c6d7" } }); expect(invalid.body.result.structuredContent).toMatchObject({ outcome: "rejected", phase: "validation", issues: [{ path: "requestId", code: "invalid_format" }] }); expect(invalid.body.result.structuredContent.issues[0].message).toContain("UUID"); expect(dispatch).not.toHaveBeenCalled(); const pending = await oauth.authorize({ client_id: f.client.client_id, redirect_uri: redirectUri, resource: config.resource, response_type: "code", code_challenge: challenge, code_challenge_method: "S256" }); vi.stubEnv("PAPERCLIP_CLOUD_API_ORIGIN", "https://cloud.example.test"); try { const described = await request(app).get(`/api/mcp/requests/${pending.split("/").at(-1)}`); expect(described.body.setupUrl).toBe("https://cloud.example.test/orgs/new"); } finally { vi.unstubAllEnvs(); } const connection = (await oauth.authenticate(f.tokens.access_token)).grant; expect((await request(app).delete(`/api/mcp/connections/${connection.id}`)).status).toBe(403); expect((await request(app).delete(`/api/mcp/connections/${connection.id}`).set("Origin", config.origin)).status).toBe(204); expect((await db.select().from(mcpOauthGrants).where(eq(mcpOauthGrants.id, connection.id)))[0]?.revokedAt).not.toBeNull(); }); it("shares registration quotas across replicas and preserves consented clients during retention", async () => { const f = await fixture(); const stale = "stale-" + randomUUID(); const old = new Date(Date.now() - 2 * 86_400_000); await db.insert(mcpOauthClients).values({ id: stale, name: "Never connected", redirectUris: [redirectUri], createdAt: old }); await db.update(mcpOauthClients).set({ createdAt: old }); const replica = createPublicMcpOAuth(db, config); const registrations = await Promise.allSettled(Array.from({ length: 70 }, (_, index) => (index % 2 ? oauth : replica).register({ client_name: "Quota fixture", redirect_uris: [redirectUri] }, `quota-source-${index}`))); const accepted = registrations.filter((r): r is PromiseFulfilledResult>> => r.status === "fulfilled"); try { expect(accepted).toHaveLength(60); for (const rejected of registrations.filter(r => r.status === "rejected")) expect((rejected as PromiseRejectedResult).reason).toMatchObject({ status: 429 }); expect(await db.select().from(mcpOauthClients).where(eq(mcpOauthClients.id, stale))).toHaveLength(0); expect(await oauth.authenticate(f.tokens.access_token)).toMatchObject({ grant: { clientId: f.client.client_id } }); } finally { for (const row of accepted) await db.delete(mcpOauthClients).where(eq(mcpOauthClients.id, row.value.client_id)); } }); it("limits one registration source without denying another source", async () => { const source = randomUUID(); const sourceHash = hashMcpSecret(config.resource + ":" + source); const replica = createPublicMcpOAuth(db, config); try { const batch = await Promise.allSettled(Array.from({ length: 8 }, (_, i) => (i % 2 ? oauth : replica).register({ client_name: "Source quota", redirect_uris: [redirectUri] }, source))); expect(batch.filter(r => r.status === "fulfilled")).toHaveLength(6); await expect(replica.register({ client_name: "Other source", redirect_uris: [redirectUri] }, randomUUID())).resolves.toHaveProperty("client_id"); for (let batch = 0; batch < 4; batch++) { await db.update(mcpOauthClients).set({ createdAt: new Date(Date.now() - 2 * 60_000) }).where(eq(mcpOauthClients.registrationSourceHash, sourceHash)); for (let i = 0; i < 6; i++) await replica.register({ client_name: "Source quota", redirect_uris: [redirectUri] }, source); } await db.update(mcpOauthClients).set({ createdAt: new Date(Date.now() - 2 * 60_000) }).where(eq(mcpOauthClients.registrationSourceHash, sourceHash)); await expect(oauth.register({ client_name: "Source quota", redirect_uris: [redirectUri] }, source)).rejects.toMatchObject({ status: 429 }); } finally { await db.delete(mcpOauthClients).where(eq(mcpOauthClients.registrationSourceHash, sourceHash)); } }); it("bounds authorization starts across replicas and reclaims expired requests without another registration", async () => { const client = await oauth.register({ client_name: "Request quota", redirect_uris: [redirectUri] }, randomUUID()); const input = { client_id: client.client_id, redirect_uri: redirectUri, response_type: "code", resource: config.resource, code_challenge: challenge, code_challenge_method: "S256" }; const replica = createPublicMcpOAuth(db, config); const starts = await Promise.allSettled(Array.from({ length: 12 }, (_, i) => (i % 2 ? oauth : replica).authorize(input))); expect(starts.filter(r => r.status === "fulfilled")).toHaveLength(10); expect(starts.filter(r => r.status === "rejected").map(r => r.reason.status)).toEqual([429, 429]); await db.update(mcpOauthRequests).set({ expiresAt: new Date(0) }).where(eq(mcpOauthRequests.clientId, client.client_id)); await expect(replica.authorize(input)).resolves.toContain("/mcp-connect/"); expect(await db.select().from(mcpOauthRequests).where(eq(mcpOauthRequests.clientId, client.client_id))).toHaveLength(1); }); it("does not charge completed connections against a shared client's pending consent quota", async () => { const f = await fixture(); const input = { client_id: f.client.client_id, redirect_uri: redirectUri, response_type: "code", resource: config.resource, code_challenge: challenge, code_challenge_method: "S256" }; for (let i = 0; i < 12; i++) { const id = (await oauth.authorize(input)).split("/").at(-1)!; const consent = await oauth.consent(id, f.actor, { decision: "approve", companyId: f.company.id, allowWrites: false }); const code = new URL(consent.redirectUrl).searchParams.get("code")!; await expect(oauth.token({ grant_type: "authorization_code", client_id: f.client.client_id, redirect_uri: redirectUri, resource: config.resource, code, code_verifier: verifier })).resolves.toHaveProperty("access_token"); } await expect(oauth.authorize(input)).resolves.toContain("/mcp-connect/"); }); async function eventFixture(cloudOrigin?: string, write = false) { const f = await fixture("member", write); const principal = await oauth.authenticate(f.tokens.access_token); const [task] = await db.insert(issues).values({ companyId: f.company.id, title: "Event fixture", status: "todo" }).returning(); let clock = Date.now(); const secret = "whsec_" + randomBytes(32).toString("base64"); const received: Array<{ headers: Headers; body: any }> = []; let status = 204; let cloudAllowed = true; let goodChallenge = true; let duringVerification: (() => Promise) | undefined; const fetcher: EventFetch = async (url, init) => { if (cloudOrigin && url === cloudOrigin + "/mcp/paperclip") return cloudAllowed ? Response.json({ result: { structuredContent: { user: { id: f.actor.userId }, companyId: f.company.id, connectionId: principal.grant.id } } }) : new Response(null, { status: 403 }); const headers = new Headers(init.headers); const body = JSON.parse(String(init.body)); received.push({ headers, body }); const expected = "v1," + createHmac("sha256", Buffer.from(secret.slice(6), "base64")).update(`${headers.get("webhook-id")}.${headers.get("webhook-timestamp")}.${String(init.body)}`).digest("base64"); // A rotation test may use another secret; initial deliveries must be independently verifiable. if (!String(headers.get("webhook-signature")).includes(expected) && body.type !== "verification") throw new Error("Invalid signature"); if (body.type === "verification") { await duringVerification?.(); duringVerification = undefined; return Response.json({ challenge: goodChallenge ? body.challenge : "wrong" }); } return new Response(null, { status }); }; const dispatch = async (p: typeof principal, _method: string, path: string) => { const [row] = await db.select().from(issues).where(eq(issues.id, path.split("/").at(-1)!)); if (!row || row.companyId !== p.grant.companyId) throw new McpApiError(404); return row; }; const options = { fetch: fetcher, now: () => clock, ...(cloudOrigin ? { cloudOrigin } : {}) }; const service = createPublicMcpEvents(db, oauth, dispatch, options); const input = { name: "paperclip.task.status_changed", arguments: { companyId: f.company.id, taskId: task!.id, statuses: ["done"] }, delivery: { mode: "webhook", url: "https://receiver.example/events/" + randomUUID(), secret } }; const activity = async (action = "issue.updated", details: Record = { status: "done" }) => { clock += 100; const [row] = await db.insert(activityLog).values({ companyId: f.company.id, actorType: "user", actorId: f.actor.userId!, action, entityType: "issue", entityId: task!.id, details, createdAt: new Date(clock) }).returning(); return row!; }; return { ...f, principal, task: task!, service, input, received, secret, activity, dispatch, options, advance(ms: number) { clock += ms; }, setStatus(value: number) { status = value; }, denyCloud() { cloudAllowed = false; }, duringVerification(fn: () => Promise) { duringVerification = fn; }, badChallenge() { goodChallenge = false; }, now: () => clock }; } it("Dot prototype: OAuth → signed wakeup → native runner tools → result, with revocation", async () => { const f = await eventFixture(undefined, true); const events = createPublicMcpEvents(db, oauth, f.dispatch, { ...f.options, enableDotPrototype: true }); const extension = createDotRunnerMcpBridge(); const identity = { companyId: f.company.id, agentId: randomUUID(), issueId: f.task.id, runId: randomUUID(), sessionId: randomUUID() }; const documents: string[] = []; const driver = new DotHarnessDriver({ identity, principal: { companyId: f.company.id, grantId: f.principal.grant.id }, expiresAt: f.now() + 60_000, now: f.now, assertAuthority: async () => { await oauth.authorizeGrant(f.principal.grant.id); }, tools: [{ name: "write_document", description: "Save a report on this assignment", inputSchema: { type: "object", properties: { body: { type: "string" } }, required: ["body"], additionalProperties: false } }], executeTool: async ({ name, arguments: args }) => { if (args.uncertain) throw new Error("Projected tool response was lost"); expect(name).toBe("write_document"); documents.push(String(args.body)); return { saved: true }; }, publish: async assignment => { await f.activity("dot.work_available", { ...assignment, privateText: "MUST NOT LEAVE IN EVENT" }); }, }); const unregister = extension.register(driver, f.principal.grant.id); const backend = new HarnessDriverBackend(driver); const session = await backend.openSession({ identity }); const app = express(); app.use(express.json()); app.use(publicMcpIngressRoutes(oauth, createPublicMcpExecutor(db, oauth, f.dispatch), events, extension)); const meta = { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {} }; const rpc = (method: string, params: Record = {}, token = f.tokens.access_token) => request(app).post("/mcp/paperclip") .set("Authorization", `Bearer ${token}`).set("MCP-Protocol-Version", "2026-07-28").set("Mcp-Method", method) .set("Mcp-Name", String(params.name ?? "")).send({ jsonrpc: "2.0", id: 1, method, params: { ...params, _meta: meta } }); const tool = async (name: string, args: Record) => (await rpc("tools/call", { name, arguments: args })).body.result; try { expect((await rpc("server/discover")).body.result.capabilities).toHaveProperty("events"); expect((await rpc("events/list")).body.result.events.map((e: { name: string }) => e.name)).toContain("paperclip.dot.work_available"); const subscription = { ...f.input, name: "paperclip.dot.work_available", arguments: { companyId: f.company.id, taskId: f.task.id } }; expect((await rpc("events/subscribe", subscription)).body.result.id).toBeTruthy(); const { turnId } = await session.startTurn({ message: { role: "user", text: "Save a competitor research report." } }); await events.tick(); const deliveries = f.received.filter(r => r.body.eventId); expect(deliveries).toHaveLength(1); expect(deliveries[0]!.body.data).toMatchObject({ runId: identity.runId, agentId: identity.agentId, turnId }); expect(JSON.stringify(deliveries)).not.toContain("MUST NOT LEAVE"); const base = { companyId: identity.companyId, runId: identity.runId, turnId }; const call = (name: string, args = {}, requestId = randomUUID()) => tool(name, { ...base, requestId, ...args }); expect((await tool("paperclip_dot_inbox", { companyId: identity.companyId })).structuredContent.assignments).toHaveLength(1); expect((await call("paperclip_dot_read")).structuredContent.result.message.text).toContain("competitor"); expect((await call("paperclip_dot_accept")).isError).toBe(false); const writeId = randomUUID(); for (let retry = 0; retry < 2; retry++) expect((await call("paperclip_dot_tool", { name: "write_document", arguments: { body: "Competitor report" } }, writeId)).isError).toBe(false); expect(documents).toEqual(["Competitor report"]); const unknown = await call("paperclip_dot_tool", { name: "write_document", arguments: { uncertain: true } }); expect(unknown.isError).toBe(true); expect(unknown.structuredContent.outcome).toBe("unknown"); expect((await call("paperclip_dot_progress", { text: "Saved the report." })).isError).toBe(false); const other = await fixture(); expect((await rpc("tools/call", { name: "paperclip_dot_read", arguments: { ...base, requestId: randomUUID() } }, other.tokens.access_token)).body.result.isError).toBe(true); const result = { schema: "paperclip.run_result.v1", reportedWorkDisposition: "done", summary: "Report saved.", completionClaim: { contractRevision: "dot-prototype-v1", objectiveSatisfied: true, criteria: [], remainingWork: [] }, evidence: [], verification: [], attentionRequests: [], artifacts: [] }; expect((await call("paperclip_dot_finish", { result })).isError).toBe(false); const transcript = []; for await (const event of session.events()) transcript.push(event); expect(transcript.filter(e => e.eventType === "run.result.proposed")).toHaveLength(1); expect(await session.result()).toMatchObject({ result }); expect(await session.usage?.()).toBeNull(); // The person's normal tools remain usable while Dot has no active assignment. expect((await tool("paperclip_connection", {})).structuredContent.companyId).toBe(identity.companyId); await oauth.revokeConnection(f.principal.grant.id, f.actor.userId!); expect((await rpc("tools/list")).status).toBe(401); } finally { unregister(); await session.close({ reason: "prototype finished" }).catch(() => {}); await events.stop(); } }); it("discovers MCP 2.0 events, validates metadata/headers, and preserves legacy tools", async () => { const f = await eventFixture(); const app = express(); app.use(express.json()); app.use(publicMcpIngressRoutes(oauth, createPublicMcpExecutor(db, oauth, f.dispatch), f.service)); const params = { _meta: { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {} } }; const rpc = (method: string, extra = {}, headers: Record = {}) => request(app).post("/mcp/paperclip") .set({ Authorization: `Bearer ${f.tokens.access_token}`, "MCP-Protocol-Version": "2026-07-28", "Mcp-Method": method, ...headers }) .send({ jsonrpc: "2.0", id: 1, method, params: { ...params, ...extra } }); expect((await rpc("server/discover")).body.result).toMatchObject({ resultType: "complete", supportedVersions: ["2026-07-28"], capabilities: { tools: {}, events: {} } }); expect((await rpc("events/list")).body.result.events.map((e: { name: string }) => e.name)).toEqual(publicMcpEventDefinitions.map(e => e.name)); expect((await rpc("tools/list")).body.result.tools).toHaveLength(publicMcpCapabilities.length); expect((await rpc("tools/call", { name: "paperclip_connection", arguments: {} }, { "Mcp-Name": "=?base64?cGFwZXJjbGlwX2Nvbm5lY3Rpb24=?=" })).body.result.structuredContent.companyId).toBe(f.company.id); expect((await rpc("events/list", {}, { "Mcp-Method": "tools/list" })).body.error.code).toBe(-32020); expect((await rpc("events/list", { _meta: {} })).body.error.code).toBe(-32602); expect((await rpc("events/list", { cursor: "unknown" })).body.error.code).toBe(-32602); const subscribed = await rpc("events/subscribe", f.input); expect(subscribed.status).toBe(200); expect(subscribed.body.result.id).toMatch(/^sub_/); expect((await rpc("events/unsubscribe", { ...f.input, delivery: { mode: "webhook", url: f.input.delivery.url } })).body.result.resultType).toBe("complete"); }); it("persists verified subscriptions, deduplicates refresh and delivers signed filtered events after restart", async () => { const f = await eventFixture(); const first = await f.service.subscribe(f.principal, f.input); const reordered = { ...f.input, arguments: { statuses: ["done"], taskId: f.task.id, companyId: f.company.id } }; const concurrent = await Promise.all([f.service.subscribe(f.principal, reordered), f.service.subscribe(f.principal, f.input)]); expect(concurrent.map(s => s.id)).toEqual([first.id, first.id]); expect(f.received).toHaveLength(1); const [stored] = await db.select().from(mcpEventSubscriptions).where(eq(mcpEventSubscriptions.id, first.id)); expect(JSON.stringify(stored)).not.toContain(f.secret); expect(JSON.stringify(stored)).not.toContain(f.input.delivery.url); await f.activity("issue.updated", { status: "in_progress" }); const matched = await f.activity(); const replica = createPublicMcpEvents(db, oauth, f.dispatch, f.options); await Promise.all([f.service.tick(), replica.tick()]); expect(f.received.filter(r => r.body.eventId)).toHaveLength(1); expect(f.received.at(-1)!.body).toMatchObject({ eventId: "evt_" + matched.id, name: f.input.name, cursor: null, data: { companyId: f.company.id, taskId: f.task.id, status: "done" } }); expect(f.received.at(-1)!.headers.get("X-MCP-Subscription-Id")).toBe(first.id); await replica.tick(); expect(f.received.filter(r => r.body.eventId)).toHaveLength(1); await f.service.unsubscribe(f.principal, f.input); }); it("keeps the longest promised lifetime when cached refreshes overlap across replicas", async () => { const f = await eventFixture(); const first = await f.service.subscribe(f.principal, { ...f.input, ttlMs: 30_000 }); const replica = createPublicMcpEvents(db, oauth, f.dispatch, f.options); const refreshed = await Promise.all([ f.service.subscribe(f.principal, { ...f.input, ttlMs: 3600_000 }), replica.subscribe(f.principal, { ...f.input, ttlMs: 30_000 }), ]); const [stored] = await db.select().from(mcpEventSubscriptions).where(eq(mcpEventSubscriptions.id, first.id)); expect(stored!.expiresAt.getTime()).toBe(f.now() + 3600_000); expect(refreshed.every(r => Date.parse(r.refreshBefore) <= stored!.expiresAt.getTime())).toBe(true); const shorter = await replica.subscribe(f.principal, { ...f.input, ttlMs: 30_000 }); expect(Date.parse(shorter.refreshBefore)).toBe(stored!.expiresAt.getTime()); expect(f.received).toHaveLength(1); await f.service.unsubscribe(f.principal, f.input); }); it("retries a lost delivery with a stable ID and fresh signature, rotates keys and stops on unsubscribe", async () => { const f = await eventFixture(); await f.service.subscribe(f.principal, f.input); await f.activity(); f.setStatus(503); await f.service.tick(); const failed = f.received.at(-1)!; const replacement = "whsec_" + randomBytes(32).toString("base64"); await f.service.subscribe(f.principal, { ...f.input, delivery: { ...f.input.delivery, secret: replacement } }); f.advance(10_000); f.setStatus(204); await f.service.tick(); const retried = f.received.at(-1)!; expect(retried.body).toEqual(failed.body); expect(retried.headers.get("webhook-timestamp")).not.toBe(failed.headers.get("webhook-timestamp")); expect(retried.headers.get("webhook-signature")!.split(" ")).toHaveLength(2); await f.service.unsubscribe(f.principal, f.input); await f.service.unsubscribe(f.principal, f.input); await f.activity(); const before = f.received.length; await f.service.tick(); expect(f.received).toHaveLength(before); }); it.each([410, 413, 400])("does not retry terminal callback HTTP %s", async status => { const f = await eventFixture(); await f.service.subscribe(f.principal, f.input); await f.activity(); f.setStatus(status); await f.service.tick(); const before = f.received.length; f.advance(60_000); await f.service.tick(); expect(f.received).toHaveLength(before); await f.service.unsubscribe(f.principal, f.input); }); it("enforces expiry, fresh consent revocation, membership loss, task isolation and failed verification", async () => { const f = await eventFixture(); await expect(f.service.subscribe(f.principal, { ...f.input, arguments: { ...f.input.arguments, companyId: randomUUID() } })).rejects.toThrow(); const foreign = await fixture(); const [foreignTask] = await db.insert(issues).values({ companyId: foreign.company.id, title: "Private" }).returning(); await expect(f.service.subscribe(f.principal, { ...f.input, arguments: { ...f.input.arguments, taskId: foreignTask!.id } })).rejects.toThrow(); expect(f.received).toHaveLength(0); f.badChallenge(); await expect(f.service.subscribe(f.principal, f.input)).rejects.toMatchObject({ code: -32015, reason: "challenge_failed" }); expect(await db.select().from(mcpEventSubscriptions).where(eq(mcpEventSubscriptions.grantId, f.principal.grant.id))).toHaveLength(0); for (const reason of ["expiry", "revocation", "membership"] as const) { const g = await eventFixture(); await g.service.subscribe(g.principal, { ...g.input, ttlMs: 30000 }); await g.activity(); if (reason === "expiry") g.advance(31000); if (reason === "revocation") await oauth.revokeConnection(g.principal.grant.id, g.principal.grant.userId); if (reason === "membership") await db.update(companyMemberships).set({ status: "inactive" }).where(eq(companyMemberships.id, g.membership.id)); await g.service.tick(); expect(g.received.filter(r => r.body.eventId)).toHaveLength(0); await g.service.unsubscribe(g.principal, g.input); } }); it("retains changes at the subscription start while callback ownership is being verified", async () => { const f = await eventFixture(); f.duringVerification(async () => { f.advance(-100); await f.activity(); }); await f.service.subscribe(f.principal, f.input); await f.service.tick(); expect(f.received.filter(r => r.body.eventId)).toHaveLength(1); await f.service.unsubscribe(f.principal, f.input); }); it("pauses queued event delivery and new subscriptions while the experimental switch is off", async () => { const f = await eventFixture(); const settings = instanceSettingsService(db); await f.service.subscribe(f.principal, f.input); await f.activity(); f.setStatus(503); await f.service.tick(); const count = f.received.length; try { await settings.updateExperimental({ enablePublicMcp: false }); f.advance(10_000); f.setStatus(204); await f.service.tick(); expect(f.received).toHaveLength(count); await expect(f.service.subscribe(f.principal, f.input)).rejects.toMatchObject({ status: 503 }); await expect(oauth.authorizeGrant(f.principal.grant.id)).rejects.toMatchObject({ status: 503 }); } finally { await settings.updateExperimental({ enablePublicMcp: true }); } await f.service.tick(); expect(f.received).toHaveLength(count + 1); await f.service.unsubscribe(f.principal, f.input); }); it("preserves the final delivery attempt when access is disabled after the tick begins", async () => { const f = await eventFixture(); const settings = instanceSettingsService(db); const subscription = await f.service.subscribe(f.principal, f.input); await f.activity(); f.setStatus(503); await f.service.tick(); await db.update(mcpEventDeliveries).set({ attempts: 5 }).where(eq(mcpEventDeliveries.subscriptionId, subscription.id)); f.advance(10_000); f.setStatus(204); const count = f.received.length; const authorize = oauth.authorizeGrant.bind(oauth); const gate = vi.spyOn(oauth, "authorizeGrant").mockImplementationOnce(async id => { await settings.updateExperimental({ enablePublicMcp: false }); return authorize(id); }); try { await f.service.tick(); expect(f.received).toHaveLength(count); const [paused] = await db.select().from(mcpEventDeliveries).where(eq(mcpEventDeliveries.subscriptionId, subscription.id)); expect(paused).toMatchObject({ attempts: 5, finishedAt: null, outcome: "paused" }); } finally { gate.mockRestore(); await settings.updateExperimental({ enablePublicMcp: true }); } await f.service.tick(); expect(f.received).toHaveLength(count + 1); const [delivered] = await db.select().from(mcpEventDeliveries).where(eq(mcpEventDeliveries.subscriptionId, subscription.id)); expect(delivered).toMatchObject({ attempts: 6, outcome: "delivered" }); expect(delivered!.finishedAt).not.toBeNull(); await f.service.unsubscribe(f.principal, f.input); }); it("reclaims stopped and expired monitors before admitting another subscription", async () => { const f = await eventFixture(); const makeInput = (index: number) => ({ ...f.input, delivery: { ...f.input.delivery, url: f.input.delivery.url + "/" + index } }); for (let i = 0; i < 20; i++) await f.service.subscribe(f.principal, makeInput(i)); const verified = f.received.length; await expect(f.service.subscribe(f.principal, makeInput(20))).rejects.toMatchObject({ code: -32602 }); expect(f.received).toHaveLength(verified); await db.update(mcpEventSubscriptions).set({ stoppedAt: new Date(f.now()) }).where(eq(mcpEventSubscriptions.grantId, f.principal.grant.id)); await expect(f.service.subscribe(f.principal, makeInput(20))).resolves.toHaveProperty("id"); expect(await db.select().from(mcpEventSubscriptions).where(eq(mcpEventSubscriptions.grantId, f.principal.grant.id))).toHaveLength(1); await db.update(mcpEventSubscriptions).set({ expiresAt: new Date(f.now() - 1) }).where(eq(mcpEventSubscriptions.grantId, f.principal.grant.id)); await expect(f.service.subscribe(f.principal, makeInput(21))).resolves.toHaveProperty("id"); expect(await db.select().from(mcpEventSubscriptions).where(eq(mcpEventSubscriptions.grantId, f.principal.grant.id))).toHaveLength(1); await f.service.unsubscribe(f.principal, makeInput(21)); }); it("bounds failed callback attempts across service replicas before doing network work", async () => { const f = await eventFixture(); f.badChallenge(); const replica = createPublicMcpEvents(db, oauth, f.dispatch, f.options); const input = (index: number) => ({ ...f.input, delivery: { ...f.input.delivery, url: f.input.delivery.url + "/" + index } }); for (let i = 0; i < 30; i++) await expect((i % 2 ? replica : f.service).subscribe(f.principal, input(i))).rejects.toMatchObject({ reason: "challenge_failed" }); expect(f.received).toHaveLength(30); await expect(replica.subscribe(f.principal, input(30))).rejects.toThrow("capacity reached"); expect(f.received).toHaveLength(30); expect(await db.select().from(mcpEventAdmissions).where(eq(mcpEventAdmissions.grantId, f.principal.grant.id))).toHaveLength(30); f.advance(60_001); await expect(replica.subscribe(f.principal, input(31))).rejects.toMatchObject({ reason: "challenge_failed" }); expect(f.received).toHaveLength(31); expect(await db.select().from(mcpEventAdmissions).where(eq(mcpEventAdmissions.grantId, f.principal.grant.id))).toHaveLength(1); }); it("reserves bounded verification capacity without holding transactions and respects unsubscribe", async () => { const f = await eventFixture(); const replica = createPublicMcpEvents(db, oauth, f.dispatch, f.options); let release!: () => void; const gate = new Promise(resolve => { release = resolve; }); f.duringVerification(() => gate); const input = (index: number) => ({ ...f.input, delivery: { ...f.input.delivery, url: f.input.delivery.url + "/" + index } }); const pending = Promise.allSettled([f.service.subscribe(f.principal, input(0)), replica.subscribe(f.principal, input(1))]); try { await vi.waitFor(() => expect(f.received).toHaveLength(2)); const held = await db.select().from(mcpEventAdmissions).where(eq(mcpEventAdmissions.grantId, f.principal.grant.id)); expect(held).toHaveLength(2); await db.transaction(async tx => { const [locks] = await tx.execute(sql`select pg_try_advisory_xact_lock(736721043) as global_free, pg_try_advisory_xact_lock(hashtextextended(${held[0]!.subscriptionId}, 0)) as subscription_free`); expect(locks).toMatchObject({ global_free: true, subscription_free: true }); }); await expect(replica.subscribe(f.principal, input(2))).rejects.toThrow("capacity reached"); expect(f.received).toHaveLength(2); await f.service.unsubscribe(f.principal, input(0)); } finally { release(); } const results = await pending; expect(results.map(result => result.status)).toEqual(["rejected", "fulfilled"]); expect(await db.select().from(mcpEventSubscriptions).where(eq(mcpEventSubscriptions.grantId, f.principal.grant.id))).toHaveLength(1); await replica.unsubscribe(f.principal, input(1)); }); it.each(["lease", "revocation"])("rejects stale %s authority after callback verification", async reason => { const f = await eventFixture(); f.duringVerification(async () => { if (reason === "lease") f.advance(60_001); else await oauth.revokeConnection(f.principal.grant.id, f.principal.grant.userId); }); await expect(f.service.subscribe(f.principal, f.input)).rejects.toThrow(); expect(await db.select().from(mcpEventSubscriptions).where(eq(mcpEventSubscriptions.grantId, f.principal.grant.id))).toHaveLength(0); }); it("filters unchanged statuses and includes native checkout and release transitions", async () => { const f = await eventFixture(); const input = { ...f.input, arguments: { companyId: f.company.id, taskId: f.task.id } }; await f.service.subscribe(f.principal, input); await f.activity("issue.updated", { status: "done", changes: { title: { from: "a", to: "b" } } }); await f.activity("issue.checked_out", { status: "in_progress", _previous: { status: "in_progress" } }); await f.activity("issue.checked_out", { status: "in_progress", _previous: { status: "todo" } }); await f.activity("issue.released", { status: "todo", _previous: { status: "in_progress" } }); await f.service.tick(); const delivered = f.received.filter(r => r.body.eventId).map(r => r.body.data.status); // Independent webhook deliveries do not promise arrival order. expect(delivered).toHaveLength(2); expect(delivered).toEqual(expect.arrayContaining(["in_progress", "todo"])); await f.service.unsubscribe(f.principal, input); }); it("bounds lifetimes and retries without claiming unsupported replay", async () => { const f = await eventFixture(); const short = await f.service.subscribe(f.principal, { ...f.input, ttlMs: 1 }); expect(Date.parse(short.refreshBefore)).toBe(f.now() + 30_000); const unlimited = await f.service.subscribe(f.principal, { ...f.input, ttlMs: null }); expect(Date.parse(unlimited.refreshBefore)).toBe(f.now() + 24 * 3600_000); expect(unlimited.cursor).toBeNull(); await expect(f.service.subscribe(f.principal, { ...f.input, cursor: "pretend-replay" })).rejects.toThrow(); await f.activity(); f.setStatus(503); for (let attempt = 0; attempt < 8; attempt++) { await f.service.tick(); f.advance(3600_000); } expect(f.received.filter(r => r.body.eventId)).toHaveLength(6); expect(new Set(f.received.filter(r => r.body.eventId).map(r => r.body.eventId)).size).toBe(1); await f.service.unsubscribe(f.principal, f.input); }); it("does not skip an older activity that commits after a newer event was delivered", async () => { const f = await eventFixture(); await f.service.subscribe(f.principal, f.input); let release!: () => void; let inserted!: () => void; const gate = new Promise(resolve => { release = resolve; }); const ready = new Promise(resolve => { inserted = resolve; }); const lateId = randomUUID(); const transaction = db.transaction(async tx => { await tx.insert(activityLog).values({ id: lateId, companyId: f.company.id, actorType: "user", actorId: f.actor.userId!, action: "issue.updated", entityType: "issue", entityId: f.task.id, details: { status: "done" }, createdAt: new Date(f.now() + 1) }); inserted(); await gate; }); try { await ready; const newer = await f.activity(); await f.service.tick(); expect(f.received.filter(r => r.body.eventId).map(r => r.body.eventId)).toEqual(["evt_" + newer.id]); } finally { release(); await transaction; } await f.service.tick(); expect(f.received.filter(r => r.body.eventId).at(-1)!.body.eventId).toBe("evt_" + lateId); await f.service.unsubscribe(f.principal, f.input); }); it("sends only comment/document references and checks hosted membership on each delivery", async () => { const f = await eventFixture("https://cloud.example"); const authority = { token: "fixture-cloud-proof", expiresAt: f.now() + 60_000 }; await expect(f.service.subscribe(f.principal, f.input)).rejects.toThrow(); const subscription = await f.service.subscribe(f.principal, f.input, authority); expect(new Date(subscription.refreshBefore).getTime()).toBe(authority.expiresAt); await f.activity(); f.denyCloud(); await f.service.tick(); expect(f.received.filter(r => r.body.eventId)).toHaveLength(0); await f.service.unsubscribe(f.principal, f.input); const g = await eventFixture(); for (const [name, action, details] of [ ["paperclip.task.comment_created", "issue.comment_added", { commentId: randomUUID(), body: "private comment" }], ["paperclip.task.document_updated", "issue.document_updated", { key: "report", revisionNumber: 2, body: "private report" }], ] as const) { const input = { ...g.input, name, arguments: { companyId: g.company.id, taskId: g.task.id } }; await g.service.subscribe(g.principal, input); await g.activity(action, details); await g.service.tick(); expect(g.received.at(-1)!.body.name).toBe(name); expect(JSON.stringify(g.received.at(-1)!.body)).not.toContain("private"); await g.service.unsubscribe(g.principal, input); } }); }); describe("MCP event outbound boundaries", () => { it.each(["https://127.0.0.1/", "https://169.254.169.254/", "https://[::1]/", "https://localhost/"])("rejects callback destination %s before sending", async url => { await expect(eventFetch(url, { method: "POST", body: "private" })).rejects.toMatchObject({ code: -32015, reason: "invalid_destination" }); }); it("rejects malformed, short and long signing material", () => { for (const secret of ["bad", "whsec_%%%", "whsec_" + Buffer.alloc(23).toString("base64"), "whsec_" + Buffer.alloc(65).toString("base64")]) expect(() => signingKey(secret)).toThrow(); expect(signingKey("whsec_" + Buffer.alloc(32).toString("base64"))).toHaveLength(32); }); });