mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-11 14:10:50 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - App connections must work in both the operator UI and agent tool gateway. > - The first stack layer adds secure remote connections. > - Operators still need clear setup, test, and recovery states. > - This pull request adds the gateway behavior and the workspace connection experience. > - The benefit is a connection flow that is easier to understand and recover. ## Linked Issues or Issue Description Refs #11965 This is stack 2 of 11. It depends on stack 1 and replaces another reviewable part of #11965. ## What Changed - Improve remote tool gateway connection behavior. - Add clearer app setup, test, and recovery states. - Add focused server and UI tests for the new paths. - Keep the diff isolated from later identity and catalog work. - Stabilize DNS-pinned remote HTTP protocol fixtures and the managed-runtime public-origin fixture for this independently tested layer. ## Verification - `pnpm -r typecheck` - `pnpm --filter @paperclipai/server exec vitest run src/__tests__/tool-access-service.test.ts` (150 passed) - `pnpm test:run` - `pnpm check:token-gates` - `pnpm build` ## Risks - Gateway errors now surface through new user-facing states. - A stale connection can require a new setup attempt. - The change does not add a database migration. - The injected HTTP transport and public URL are test-only fixtures; production DNS pinning and runtime behavior are unchanged. > I checked `ROADMAP.md`. This stack continues the existing app connection work from #11965 and does not duplicate another planned item. ## Model Used OpenAI Codex, GPT-5. The runtime model ID and context window were not exposed. The model used reasoning, tool use, and code execution. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links - [x] My branch name describes the change and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
102 lines
3.5 KiB
TypeScript
102 lines
3.5 KiB
TypeScript
import { createDb } from "@paperclipai/db";
|
|
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
|
import type { Config } from "../config.js";
|
|
import { createBetterAuthInstance } from "../auth/better-auth.js";
|
|
import {
|
|
getEmbeddedPostgresTestSupport,
|
|
startEmbeddedPostgresTestDatabase,
|
|
} from "./helpers/embedded-postgres.js";
|
|
|
|
const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
|
|
const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip;
|
|
|
|
if (!embeddedPostgresSupport.supported) {
|
|
console.warn(
|
|
`Skipping managed loopback auth tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`,
|
|
);
|
|
}
|
|
|
|
function authRequest(origin: string, path: string, init?: RequestInit): Request {
|
|
return new Request(`${origin}/api/auth${path}`, {
|
|
...init,
|
|
headers: {
|
|
origin,
|
|
"content-type": "application/json",
|
|
...init?.headers,
|
|
},
|
|
});
|
|
}
|
|
|
|
function sessionCookie(response: Response): string {
|
|
const cookie = response.headers
|
|
.getSetCookie()
|
|
.find((value) => value.includes(".session_token="));
|
|
expect(cookie).toBeDefined();
|
|
return cookie!;
|
|
}
|
|
|
|
describeEmbeddedPostgres("managed runtime loopback auth cookies", () => {
|
|
const publicOrigin = "https://worktree.example.test";
|
|
const loopbackOrigin = "http://127.0.0.1:42013";
|
|
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
|
|
|
|
beforeAll(async () => {
|
|
tempDb = await startEmbeddedPostgresTestDatabase("paperclip-managed-loopback-auth-");
|
|
}, 20_000);
|
|
|
|
afterAll(async () => {
|
|
vi.unstubAllEnvs();
|
|
await tempDb?.cleanup();
|
|
});
|
|
|
|
it("uses a loopback sign-in cookie on the next request while keeping the public cookie secure", async () => {
|
|
vi.stubEnv("BETTER_AUTH_SECRET", "managed-loopback-auth-test-secret");
|
|
vi.stubEnv("PAPERCLIP_MANAGED_RUNTIME_PUBLIC_URL", publicOrigin);
|
|
vi.stubEnv("PAPERCLIP_PUBLIC_URL", "");
|
|
|
|
const db = createDb(tempDb!.connectionString);
|
|
const config = {
|
|
deploymentMode: "authenticated",
|
|
deploymentExposure: "private",
|
|
authBaseUrlMode: "explicit",
|
|
authPublicBaseUrl: publicOrigin,
|
|
authDisableSignUp: false,
|
|
} as Config;
|
|
const auth = createBetterAuthInstance(db, config, [publicOrigin, loopbackOrigin]);
|
|
const credentials = {
|
|
name: "Loopback Operator",
|
|
email: "loopback-operator@example.test",
|
|
password: "correct-horse-battery-staple",
|
|
};
|
|
|
|
const signUpResponse = await auth.handler(authRequest(publicOrigin, "/sign-up/email", {
|
|
method: "POST",
|
|
body: JSON.stringify(credentials),
|
|
}));
|
|
expect(signUpResponse.status).toBe(200);
|
|
expect(sessionCookie(signUpResponse)).toMatch(/;\s*Secure(?:;|$)/i);
|
|
|
|
const signInResponse = await auth.handler(authRequest(loopbackOrigin, "/sign-in/email", {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
email: credentials.email,
|
|
password: credentials.password,
|
|
}),
|
|
}));
|
|
expect(signInResponse.status).toBe(200);
|
|
const loopbackCookie = sessionCookie(signInResponse);
|
|
expect(loopbackCookie).not.toMatch(/;\s*Secure(?:;|$)/i);
|
|
|
|
const getSessionResponse = await auth.handler(authRequest(loopbackOrigin, "/get-session", {
|
|
method: "GET",
|
|
headers: {
|
|
cookie: loopbackCookie.split(";", 1)[0],
|
|
},
|
|
}));
|
|
expect(getSessionResponse.status).toBe(200);
|
|
await expect(getSessionResponse.json()).resolves.toMatchObject({
|
|
user: { email: credentials.email },
|
|
});
|
|
});
|
|
});
|