mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-30 21:49:18 +01:00
docs(api): publish X setup and task tool contracts
Document board and task/run authentication and preserve provider-only webhook coverage exclusions. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
a9070242d1
commit
b4d81f3905
3 files changed
+54
-3
No files matched your search
@@ -95,6 +95,7 @@ const explicitOpenApiOperationCoverageExclusions = new Set([
|
||||
// Paperclip board/agent credential. It intentionally stays out of the public
|
||||
// board API document, while this exact exclusion keeps route coverage honest.
|
||||
"POST /api/chat-webhooks/agentmail/{publicId}",
|
||||
"GET /api/chat-webhooks/{publicId}/{provider}",
|
||||
"POST /api/chat-webhooks/{publicId}/{provider}",
|
||||
]);
|
||||
|
||||
@@ -485,7 +486,7 @@ describe("openapi routes", () => {
|
||||
properties: {
|
||||
provider: {
|
||||
type: "string",
|
||||
enum: ["slack", "github", "discord", "microsoft-teams", "telegram", "imessage-photon"],
|
||||
enum: ["x", "slack", "github", "discord", "microsoft-teams", "telegram", "imessage-photon"],
|
||||
},
|
||||
assignedAgentId: { type: "string", format: "uuid" },
|
||||
},
|
||||
@@ -710,6 +711,25 @@ describe("openapi routes", () => {
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("documents X setup as board-only and tools as task/run-bound", () => {
|
||||
const { spec } = loadSpecRoutes();
|
||||
for (const [method, routePath] of [
|
||||
["post", "/api/chat-endpoints/{endpointId}/x/authorize"],
|
||||
["get", "/api/chat-endpoints/{endpointId}/x/identity"],
|
||||
["put", "/api/chat-endpoints/{endpointId}/x/progress"],
|
||||
["post", "/api/chat-endpoints/{endpointId}/x/finish"],
|
||||
["get", "/api/x/oauth/callback"],
|
||||
["get", "/api/x/identity/{confirmationId}"],
|
||||
["post", "/api/x/identity/{confirmationId}/confirm"],
|
||||
]) {
|
||||
expect(spec.paths[routePath][method]["x-paperclip-authorization"]).toEqual({ actor: "board" });
|
||||
}
|
||||
const tool = spec.paths["/api/companies/{companyId}/x/tasks/{issueId}/tools"].post;
|
||||
expect(tool["x-paperclip-authorization"]).toEqual({ actor: "agent", heartbeatBound: true, taskBound: true });
|
||||
expect(tool.requestBody.content["application/json"].schema.properties.tool.enum).toEqual(["x_read_thread", "x_reply", "x_delivery"]);
|
||||
expect(spec.paths["/api/chat-webhooks/{publicId}/{provider}"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("covers the mounted server routes exactly", () => {
|
||||
const {
|
||||
routes: actualRoutes,
|
||||
|
||||
@@ -621,7 +621,8 @@ export function chatWebhookRoutes(
|
||||
windowMs: CHAT_WEBHOOK_RATE_LIMIT_WINDOW_MS,
|
||||
maxRequests: CHAT_WEBHOOK_RATE_LIMIT_MAX_REQUESTS,
|
||||
});
|
||||
router.route("/api/chat-webhooks/:publicId/:provider").get(webhook).post(webhook);
|
||||
router.get("/api/chat-webhooks/:publicId/:provider", webhook);
|
||||
router.post("/api/chat-webhooks/:publicId/:provider", webhook);
|
||||
async function webhook(req: ExpressRequest, res: ExpressResponse) {
|
||||
if (req.method === "GET" && req.params.provider !== "x") throw badRequest("GET challenges are only supported for X");
|
||||
recordChatWebhookStage("handler_started");
|
||||
|
||||
@@ -795,6 +795,8 @@ const chatEndpointSetupResponseSchema = z
|
||||
.strict()
|
||||
.optional(),
|
||||
callbacksNeedUpdate: z.boolean().optional(),
|
||||
x: z.object({ stage: z.number().int().min(1).max(5), clientConfigured: z.boolean().optional() }).optional(),
|
||||
testSkipped: z.boolean().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
@@ -1491,6 +1493,13 @@ const BOARD_ONLY_OPERATIONS = new Set([
|
||||
// policy, and replay controls. Every mounted handler asserts a board actor;
|
||||
// keep the generated security contract equally restrictive.
|
||||
"GET /api/slack/search/callback",
|
||||
"POST /api/chat-endpoints/{endpointId}/x/authorize",
|
||||
"GET /api/chat-endpoints/{endpointId}/x/identity",
|
||||
"PUT /api/chat-endpoints/{endpointId}/x/progress",
|
||||
"POST /api/chat-endpoints/{endpointId}/x/finish",
|
||||
"GET /api/x/oauth/callback",
|
||||
"GET /api/x/identity/{confirmationId}",
|
||||
"POST /api/x/identity/{confirmationId}/confirm",
|
||||
"GET /api/companies/{companyId}/slack/endpoints/{endpointId}/capabilities",
|
||||
"GET /api/companies/{companyId}/slack/endpoints/{endpointId}/search",
|
||||
"PUT /api/companies/{companyId}/slack/endpoints/{endpointId}/search",
|
||||
@@ -1635,7 +1644,7 @@ function resolveOperationAuthLevel(
|
||||
): OpenApiAuthLevel {
|
||||
const key = operationKey(method, path);
|
||||
if (PUBLIC_OPERATIONS.has(key)) return "public";
|
||||
if (key === "POST /api/mcp/project-tools" || key === "POST /api/companies/{companyId}/slack/tasks/{issueId}/tools") return "agent_run";
|
||||
if (key === "POST /api/mcp/project-tools" || key === "POST /api/companies/{companyId}/slack/tasks/{issueId}/tools" || key === "POST /api/companies/{companyId}/x/tasks/{issueId}/tools") return "agent_run";
|
||||
if (RUNTIME_TOOLS_OPERATIONS.has(key)) return "runtime_tools";
|
||||
if (INSTANCE_ADMIN_OPERATIONS.has(key)) return "instance_admin";
|
||||
if (
|
||||
@@ -2138,6 +2147,27 @@ registry.registerPath({ method: "get", path: "/api/slack/search/callback", tags:
|
||||
|
||||
// ─── Chat Channels ─────────────────────────────────────────────────────────
|
||||
|
||||
for (const [method, path, summary, body] of [
|
||||
["post", "/api/chat-endpoints/{endpointId}/x/authorize", "Authorize the X bot or link a personal X identity", z.object({ purpose: z.enum(["bot", "identity"]), client: z.object({ clientId: z.string().min(1), clientSecret: z.string().min(1) }).strict().optional() }).strict()],
|
||||
["get", "/api/chat-endpoints/{endpointId}/x/identity", "Read the signed-in user's X identity link", undefined],
|
||||
["put", "/api/chat-endpoints/{endpointId}/x/progress", "Save X setup progress", z.object({ stage: z.number().int().min(1).max(5) }).strict()],
|
||||
["post", "/api/chat-endpoints/{endpointId}/x/finish", "Finish X setup after verified delivery and identity linking", undefined],
|
||||
["get", "/api/x/identity/{confirmationId}", "Preview a private X identity confirmation", undefined],
|
||||
["post", "/api/x/identity/{confirmationId}/confirm", "Confirm a personal X identity against the signed-in account", undefined],
|
||||
["post", "/api/companies/{companyId}/x/tasks/{issueId}/tools", "Execute a conversation-bound X tool", z.object({ tool: z.enum(["x_read_thread", "x_reply", "x_delivery"]), arguments: z.record(z.string(), z.unknown()) }).strict()],
|
||||
] as const) {
|
||||
registry.registerPath({ method, path, tags: ["chat-channels"], summary,
|
||||
description: "Experimental X connector. Current company, account, participant and task/run authority are rechecked. Only x_reply queues a public post; its exact text and invoking target are immutable. Reuse the UUID idempotencyKey and check x_delivery after uncertainty. Internal task activity never posts. Human OAuth tokens cannot replace bot credentials.",
|
||||
request: { params: z.object(Object.fromEntries([...path.matchAll(/\{([^}]+)\}/g)].map(match => [match[1], z.string().uuid()]))), ...(body ? { body: jsonBody(body) } : {}) },
|
||||
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound, 409: r.conflict, 422: r.unprocessable },
|
||||
});
|
||||
}
|
||||
registry.registerPath({ method: "get", path: "/api/x/oauth/callback", tags: ["chat-channels"], summary: "Complete user-bound X PKCE authorization",
|
||||
description: "Consumes single-use state for the same signed-in Paperclip account. Bot authorization also requires current connection management permission. Redirects to setup or private identity confirmation.",
|
||||
request: { query: z.object({ state: z.string(), code: z.string() }) },
|
||||
responses: { 302: { description: "Redirect to X setup or identity confirmation" }, 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound, 409: r.conflict },
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/api/companies/{companyId}/chat-endpoints",
|
||||
|
||||
Reference in new issue
Block a user