mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-22 16:51:28 +02:00
## Thinking Path > - Paperclip manages AI agents and their work. > - Connections give agents governed access to external tools. > - Agents need durable memory across tasks and execution environments. > - Mem0, Zep, Supermemory, Cognee, and Honcho provide hosted memory tools. > - This pull request adds their setup flows behind an experimental toggle. > - It also delivers assigned MCP tools through the native remote Codex runner. > - Operators can connect a provider once and use the same governed tools locally or in Daytona. ## Linked Issues or Issue Description **Problem or motivation** The Apps catalog lacks a complete set of memory providers. Remote native Codex agents also need access to assigned managed MCP tools without receiving provider credentials. **Proposed solution** Add five memory connectors behind the disabled-by-default Experimental memory connectors setting. Use the existing connection setup and permissions UI. Default their tools to Allowed. Preserve company boundaries, operator permission changes, provider scopes, and audit attribution. **Alternatives considered** Direct provider credentials in each sandbox would duplicate setup and bypass the managed gateway. The remote runner instead uses its existing protocol channel to call the gateway on the server. **Roadmap alignment** This maintainer-requested experiment supports the Memory / Knowledge and Connected Apps roadmap areas. It adds provider connections without introducing a separate memory UI. Related connector authoring documentation is tracked in #13692; no duplicate memory-provider implementation was found. ## What Changed - Add provider definitions, official branding, and the experimental setting for all five providers. - Use OAuth for Zep and Supermemory, API credentials for Mem0 and Honcho, and a bundled Cloud API bridge for Cognee with no runtime downloads or subprocesses. - Default memory tools to Allowed and classify destructive actions explicitly. - Fix personal remote credential resolution and propagate provider tool errors. - Relay assigned managed MCP tools to remote native Codex through the runner protocol. Recheck current authority for each call and rotate stale tool contracts. - Add 21 Storybook states and complete OAuth walkthrough fixtures. - Document provider research, sanitized tool inventory, and live local and Daytona proof. ## Verification - Passed workspace typecheck: `pnpm -r typecheck`. - Passed production build: `pnpm build`. - Full local `pnpm test:run`: 13,273 passed, with failures from process/readiness timeouts under parallel load. Reran all 16 affected suites with one worker: 456 passed, leaving two macOS `/var` versus `/private/var` path assertions. Both passed with `TMPDIR=/private/tmp`. No test failures remain unverified. Latest-head remote CI passes all 54 checks (two optional Storybook jobs skipped). Greptile is 5/5 with no unresolved findings. - Latest Cognee gateway regression: 71 passed, including public deployment without a runtime host and immediate recovery after a provider error. Bundled bridge tests: 25 passed. - Browser setup and real agent tasks exercised all five providers. Mem0, Cognee, Zep, and Honcho have successful store/retrieve proof. - Supermemory now has scoped read/write consent. Local storage and real Daytona write, document read, and semantic recall passed; indexing completion was verified before claiming success. - All five providers were exercised through a real Daytona sandbox and its native runner MCP relay. Provider credentials remained on the server. The final bundled Cognee bridge also passed a fresh Daytona store/recall run. All disposable sandboxes were removed and verified absent after testing. - Storybook is rebuilt and contains the experimental toggle, catalog, setup, permissions, and error states. The Zep and Supermemory access steps advance correctly. ## Risks - Provider OAuth scopes and plan limits remain independent of Paperclip tool permissions. An Allowed tool can still be rejected by the provider. - Providers can queue memory indexing; save acceptance does not prove that semantic recall is ready. - Remote tool contracts must stay synchronized with current connection authority. Regression tests cover revocation and stale contracts. - This change has no database migration. Existing connections remain usable when the experimental catalog toggle is disabled. ## Model Used OpenAI Codex, GPT-6 Astra (`gpt-6-astra`), with reasoning, repository edits, code execution, and browser automation. The exact context window size is not exposed in this session. Live acceptance agents used `gpt-5.6-sol`. ## 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 (e.g. `docs/...`, `fix/...`) 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>
116 lines
5.7 KiB
TypeScript
116 lines
5.7 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
|
|
/** Reviewed Cognee Cloud contract from cognee-mcp 0.5.5. Executed by the bundled bridge below. */
|
|
export const COGNEE_STDIO_TEMPLATE = {
|
|
name: "Cognee Cloud",
|
|
command: null,
|
|
args: [] as string[],
|
|
envKeys: ["COGNEE_BASE_URL", "COGNEE_API_KEY"],
|
|
tools: [
|
|
{
|
|
name: "remember", description: "Store text in a Cognee dataset or session.",
|
|
annotations: { readOnlyHint: false },
|
|
inputSchema: { type: "object", properties: {
|
|
data: { type: "string" }, dataset_name: { type: "string" },
|
|
session_id: { type: "string" }, custom_prompt: { type: "string" },
|
|
}, required: ["data"], additionalProperties: false },
|
|
},
|
|
{
|
|
name: "recall", description: "Search Cognee memory, optionally within named datasets or a session.",
|
|
annotations: { readOnlyHint: true },
|
|
inputSchema: { type: "object", properties: {
|
|
query: { type: "string" }, search_type: { type: "string" }, datasets: { type: "string" },
|
|
session_id: { type: "string" }, system_prompt: { type: "string" }, top_k: { type: "integer", minimum: 1, default: 15 },
|
|
}, required: ["query"], additionalProperties: false },
|
|
},
|
|
{
|
|
name: "forget", description: "Permanently delete a Cognee dataset, or all owned memory when everything is true.",
|
|
annotations: { destructiveHint: true },
|
|
inputSchema: { type: "object", properties: {
|
|
dataset: { type: "string" }, everything: { type: "boolean", default: false },
|
|
}, additionalProperties: false },
|
|
},
|
|
],
|
|
};
|
|
|
|
export function cogneeCloudUrl(value: string): URL {
|
|
const url = new URL(value);
|
|
if (url.protocol !== "https:" || !/^[a-z0-9-]+\.aws\.cognee\.ai$/i.test(url.hostname)
|
|
|| url.port || url.username || url.password || url.search || url.hash || url.pathname !== "/") {
|
|
throw new Error("Copy the tenant API Base URL from Cognee's API Keys page.");
|
|
}
|
|
return url;
|
|
}
|
|
|
|
/**
|
|
* Bundled bridge for the three reviewed Cloud operations. No package manager,
|
|
* subprocess, runtime downloads, or inherited host environment is involved.
|
|
* The gateway supplies its DNS/redirect-guarded, response-bounded HTTP client.
|
|
*/
|
|
export async function callCogneeCloud(input: {
|
|
baseUrl: string;
|
|
apiKey: string;
|
|
tool: string;
|
|
parameters: Record<string, unknown>;
|
|
request: (url: URL, init: RequestInit) => Promise<unknown>;
|
|
signal: AbortSignal;
|
|
}) {
|
|
const base = cogneeCloudUrl(input.baseUrl);
|
|
if (!input.apiKey) throw new Error("Reconnect Cognee to restore its Cloud API key.");
|
|
const tenant = /^tenant-([0-9a-f-]{36})\./i.exec(base.hostname)?.[1];
|
|
const headers: Record<string, string> = { "X-Api-Key": input.apiKey };
|
|
if (tenant) headers["X-Tenant-Id"] = tenant;
|
|
const args = input.parameters;
|
|
const post = async (path: string, body: Record<string, unknown> | FormData) => {
|
|
const url = new URL(path, base);
|
|
const request = new Request(url, { method: "POST",
|
|
headers: body instanceof FormData ? headers : { ...headers, "Content-Type": "application/json" },
|
|
body: body instanceof FormData ? body : JSON.stringify(body) });
|
|
// The guarded client pins DNS through node:http and accepts byte buffers,
|
|
// not fetch's FormData. Serialize with the matching multipart boundary.
|
|
return input.request(url, { method: "POST", signal: input.signal,
|
|
headers: Object.fromEntries(request.headers), body: Buffer.from(await request.arrayBuffer()), redirect: "manual" });
|
|
};
|
|
let result: unknown;
|
|
if (input.tool === "remember") {
|
|
const dataset = args.dataset_name || "paperclip_memory";
|
|
if (args.session_id) {
|
|
if (args.custom_prompt) throw new Error("custom_prompt is not supported with session_id in Cognee Cloud.");
|
|
result = await post("/api/v1/remember/entry", {
|
|
entry: { type: "qa", question: "", answer: args.data, context: "" },
|
|
dataset_name: dataset, session_id: args.session_id,
|
|
});
|
|
} else {
|
|
const form = new FormData();
|
|
form.set("data", new Blob([String(args.data)], { type: "text/plain" }), `memory_${createHash("sha256").update(String(args.data)).digest("hex")}.txt`);
|
|
form.set("datasetName", String(dataset));
|
|
if (args.custom_prompt) form.set("custom_prompt", String(args.custom_prompt));
|
|
result = await post("/api/v1/remember", form);
|
|
}
|
|
} else if (input.tool === "recall") {
|
|
let datasets = typeof args.datasets === "string" ? args.datasets.split(",").map(v => v.trim()).filter(Boolean) : [];
|
|
if (!datasets.length && !args.session_id) {
|
|
const available = await input.request(new URL("/api/v1/datasets/", base), {
|
|
method: "GET", headers, signal: input.signal, redirect: "manual",
|
|
});
|
|
if (!Array.isArray(available)) throw new Error("Cognee returned an invalid dataset list.");
|
|
datasets = available.flatMap(value => value && typeof value.name === "string" ? [value.name] : []);
|
|
}
|
|
result = await post("/api/v1/recall", {
|
|
query: args.query, top_k: args.top_k ?? 15,
|
|
search_type: typeof args.search_type === "string" ? args.search_type.toUpperCase() : null,
|
|
...(datasets.length ? { datasets } : {}),
|
|
...(args.session_id ? { session_id: args.session_id } : {}),
|
|
...(args.system_prompt ? { system_prompt: args.system_prompt } : {}),
|
|
});
|
|
} else if (input.tool === "forget") {
|
|
if (!args.dataset && args.everything !== true) throw new Error("Specify a dataset or set everything to true.");
|
|
result = await post("/api/v1/forget", {
|
|
everything: args.everything === true, ...(args.dataset ? { dataset: args.dataset } : {}),
|
|
});
|
|
} else {
|
|
throw new Error("Unreviewed Cognee action.");
|
|
}
|
|
return { content: [{ type: "text", text: JSON.stringify(result) }], structuredContent: { result }, isError: false };
|
|
}
|