mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-06 21:05:21 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Apps give those agents governed access to external tools. > - Remote MCP setup needs secure endpoint validation and durable credentials. > - PostHog needs both browser sign-in and personal API key setup paths. > - This pull request adds the shared remote MCP foundation and the PostHog definition. > - The benefit is a secure and reusable base for later app connection work. ## Linked Issues or Issue Description Refs #11965 This is stack 1 of 11. It replaces the first reviewable part of #11965. ## What Changed - Add guarded remote MCP setup and credential handling. - Add PostHog OAuth and API key connection methods. - Add focused server, shared contract, and UI coverage. - Keep the migration replay-safe and idempotent. - Give the late-close security regression the same 10-second CI headroom as the adjacent real-timer handshake test. - Synchronize fake-timer handshake tests at the exact ensure-session boundary so real filesystem setup cannot race the fake deadline. - Drive PTY overflow coverage only after listener registration so scheduling cannot reorder the test fixture. ## Verification - pnpm exec vitest run packages/adapter-utils/src/acpx-engine/execute.test.ts server/src/__tests__/plugin-worker-manager.test.ts (220 passed; affected cases also passed five focused stress repetitions) - `pnpm exec vitest run packages/adapter-utils/src/acpx-engine/execute.test.ts -t "never leaks a sandbox-provided value from a late close rejection into logs or the result"` (1 passed) - `pnpm exec vitest run packages/adapter-utils/src/acpx-engine/execute.test.ts -t "never promotes a late ensureSession resolution|closes a late-resolving real handle exactly once"` (2 passed) - `pnpm -r typecheck` - `pnpm --filter @paperclipai/server exec vitest run src/__tests__/tool-access-service.test.ts` - `pnpm --filter @paperclipai/db check:migrations` - `pnpm build` ## Risks - Remote endpoint validation can reject configurations that previously passed without checks. - OAuth configuration errors can block setup until the operator corrects the provider settings. - The migration uses guarded statements so repeated execution is safe. - The test-only synchronization changes do not affect runtime behavior; they remove filesystem/fake-clock and listener-registration races observed under parallel CI load. > 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 (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [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>
162 lines
5.5 KiB
JavaScript
162 lines
5.5 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import {
|
|
assertSanitizedEvidence,
|
|
extractProjectSummary,
|
|
parsePosthogLiveArguments,
|
|
parseSanitizedAgentProof,
|
|
PosthogLivePreflightError,
|
|
preflightPosthogLive,
|
|
preparePosthogLiveSmoke,
|
|
} from "./posthog-live-lib.mjs";
|
|
|
|
const COMPLETE_ENV = {
|
|
PAPERCLIP_API_URL: "https://paperclip.example.test/api",
|
|
INTEGRATIONS_POSTHOG_PAPERCLIP_E2E_EMAIL: "operator@example.test",
|
|
INTEGRATIONS_POSTHOG_PAPERCLIP_DEV_LOGIN_PASSWORD: "not-a-real-password",
|
|
INTEGRATIONS_POSTHOG_POSTHOG_PROJECT_ID: "483530",
|
|
};
|
|
|
|
test("preflight reports only missing binding names", () => {
|
|
assert.throws(
|
|
() => preflightPosthogLive({
|
|
PAPERCLIP_API_URL: "https://paperclip.example.test/api",
|
|
INTEGRATIONS_POSTHOG_PAPERCLIP_DEV_LOGIN_PASSWORD: "present",
|
|
}),
|
|
(error) => {
|
|
assert.ok(error instanceof PosthogLivePreflightError);
|
|
assert.equal(error.code, "missing_environment");
|
|
assert.deepEqual(error.details.missing, [
|
|
"INTEGRATIONS_POSTHOG_PAPERCLIP_E2E_EMAIL",
|
|
"INTEGRATIONS_POSTHOG_POSTHOG_PROJECT_ID",
|
|
]);
|
|
assert.doesNotMatch(error.message, /present/);
|
|
return true;
|
|
},
|
|
);
|
|
});
|
|
|
|
test("preflight rejects credential-bearing and non-HTTPS remote URLs", () => {
|
|
for (const baseUrl of [
|
|
"https://user:secret@example.test",
|
|
"https://example.test/?code=secret",
|
|
"http://example.test",
|
|
]) {
|
|
assert.throws(
|
|
() => preflightPosthogLive({ ...COMPLETE_ENV, PAPERCLIP_API_URL: baseUrl }),
|
|
(error) => error instanceof PosthogLivePreflightError && error.code === "unsafe_base_url",
|
|
);
|
|
}
|
|
assert.equal(
|
|
preflightPosthogLive(COMPLETE_ENV, { baseUrl: "http://127.0.0.1:3100" }).baseUrl,
|
|
"http://127.0.0.1:3100",
|
|
);
|
|
});
|
|
|
|
test("preflight derives the current Paperclip origin and accepts an explicit target", () => {
|
|
assert.equal(preflightPosthogLive(COMPLETE_ENV).baseUrl, "https://paperclip.example.test");
|
|
assert.equal(
|
|
preflightPosthogLive(COMPLETE_ENV, { baseUrl: "https://other-paperclip.example.test" }).baseUrl,
|
|
"https://other-paperclip.example.test",
|
|
);
|
|
assert.throws(
|
|
() => preflightPosthogLive({ ...COMPLETE_ENV, PAPERCLIP_API_URL: "" }),
|
|
(error) => error instanceof PosthogLivePreflightError && error.code === "missing_base_url",
|
|
);
|
|
});
|
|
|
|
test("preflight fails closed unless the PostHog project is exactly 483530", () => {
|
|
assert.throws(
|
|
() => preflightPosthogLive({
|
|
...COMPLETE_ENV,
|
|
INTEGRATIONS_POSTHOG_POSTHOG_PROJECT_ID: "42",
|
|
}),
|
|
(error) => error instanceof PosthogLivePreflightError && error.code === "unexpected_project_id",
|
|
);
|
|
});
|
|
|
|
test("live smoke arguments accept a target URL without another environment binding", () => {
|
|
assert.deepEqual(parsePosthogLiveArguments([]), {});
|
|
assert.deepEqual(
|
|
parsePosthogLiveArguments(["https://paperclip.example.test"]),
|
|
{ baseUrl: "https://paperclip.example.test" },
|
|
);
|
|
assert.deepEqual(
|
|
parsePosthogLiveArguments(["--base-url", "https://paperclip.example.test"]),
|
|
{ baseUrl: "https://paperclip.example.test" },
|
|
);
|
|
assert.throws(
|
|
() => parsePosthogLiveArguments(["--unknown"]),
|
|
(error) => error instanceof PosthogLivePreflightError && error.code === "invalid_arguments",
|
|
);
|
|
});
|
|
|
|
test("browser loading happens only after binding and health preflight", async () => {
|
|
let fetchCalled = false;
|
|
let browserLoaded = false;
|
|
await assert.rejects(
|
|
preparePosthogLiveSmoke({
|
|
environment: {},
|
|
fetchImpl: async () => {
|
|
fetchCalled = true;
|
|
},
|
|
loadBrowser: async () => {
|
|
browserLoaded = true;
|
|
},
|
|
}),
|
|
(error) => error instanceof PosthogLivePreflightError && error.code === "missing_environment",
|
|
);
|
|
assert.equal(fetchCalled, false);
|
|
assert.equal(browserLoaded, false);
|
|
|
|
await assert.rejects(
|
|
preparePosthogLiveSmoke({
|
|
environment: COMPLETE_ENV,
|
|
fetchImpl: async () => ({ ok: false, status: 503 }),
|
|
loadBrowser: async () => {
|
|
browserLoaded = true;
|
|
},
|
|
}),
|
|
(error) => error instanceof PosthogLivePreflightError && error.code === "health_http_error",
|
|
);
|
|
assert.equal(browserLoaded, false);
|
|
});
|
|
|
|
test("project proof extraction retains only the expected id and name", () => {
|
|
const result = {
|
|
data: {
|
|
content: [{ type: "text", text: JSON.stringify({ id: 483530, name: "Paperclip", token: "discard-me" }) }],
|
|
},
|
|
};
|
|
assert.deepEqual(extractProjectSummary(result, "483530"), { id: "483530", name: "Paperclip" });
|
|
assert.equal(extractProjectSummary(result, "42"), null);
|
|
|
|
assert.deepEqual(
|
|
parseSanitizedAgentProof(
|
|
'{"projectId":"483530","projectName":"Paperclip","invocationId":"inv-123"}',
|
|
"483530",
|
|
),
|
|
{ projectId: "483530", projectName: "Paperclip", invocationId: "inv-123" },
|
|
);
|
|
assert.equal(
|
|
parseSanitizedAgentProof(
|
|
'Done: {"projectId":"483530","projectName":"Paperclip","invocationId":"inv-123"}',
|
|
"483530",
|
|
),
|
|
null,
|
|
);
|
|
assert.equal(
|
|
parseSanitizedAgentProof(
|
|
'{"projectId":"483530","projectName":"Paperclip","invocationId":"inv-123","token":"unsafe"}',
|
|
"483530",
|
|
),
|
|
null,
|
|
);
|
|
});
|
|
|
|
test("sanitized evidence rejects credential fields and OAuth query values", () => {
|
|
assert.doesNotThrow(() => assertSanitizedEvidence({ projectId: "483530", invocationId: "inv-123" }));
|
|
assert.throws(() => assertSanitizedEvidence({ accessToken: "secret" }), /unsafe_evidence_key/);
|
|
assert.throws(() => assertSanitizedEvidence({ note: "callback?code=secret" }), /unsafe_evidence_text/);
|
|
});
|