mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-05 03:51:54 +01:00
merge: reconcile published native prerequisite and current master
Preserve canonical startup ownership, scoped evidence, current Storybook IDs, and the Codex form controls while carrying Claude effort and immutable source bindings. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
commit
48ffca9438
415 files changed
+22501
-15340
No files matched your search
@@ -763,11 +763,6 @@ describe("AppDefinition catalog", () => {
|
||||
APP_DEFINITIONS.find((app) => app.slug === "linear")?.methods[0]?.defaults
|
||||
?.scopesHint,
|
||||
).toEqual(["read", "write"]));
|
||||
it("requests Hugging Face MCP read, contribution and job scopes", () =>
|
||||
expect(
|
||||
APP_DEFINITIONS.find((app) => app.slug === "hugging-face")?.methods[0]
|
||||
?.defaults?.scopesHint,
|
||||
).toEqual(["read-mcp", "read-repos", "contribute-repos", "jobs"]));
|
||||
it("defaults every new connection action to allowed except Enterpret writes", () => {
|
||||
for (const app of APP_DEFINITIONS)
|
||||
for (const method of app.methods) {
|
||||
@@ -1327,21 +1322,6 @@ describe("AppDefinition catalog", () => {
|
||||
?.available,
|
||||
).toBe(false);
|
||||
});
|
||||
it("enforces method and field invariants", () => {
|
||||
for (const app of APP_DEFINITIONS)
|
||||
for (const method of app.methods) {
|
||||
if (
|
||||
method.auth === "api_key" &&
|
||||
(method.purpose ?? "tool") !== "channel"
|
||||
)
|
||||
expect(method.keyPlacement).toBeTruthy();
|
||||
if (method.auth === "oauth")
|
||||
expect(method.ownershipModes.length).toBeGreaterThan(0);
|
||||
for (const field of method.credentialFields ?? [])
|
||||
if (field.required && field.type !== "checkbox")
|
||||
expect(field.placeholder).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ export function buildSlackAppManifest(input: {
|
||||
redirectUri?: string;
|
||||
}) {
|
||||
return {
|
||||
display_information: { name: input.app.appName },
|
||||
display_information: { name: input.app.appName, description: `Talk to ${input.agentName} and manage work in Paperclip.`.slice(0, 140) },
|
||||
features: {
|
||||
app_home: { home_tab_enabled: false, messages_tab_enabled: true, messages_tab_read_only_enabled: false },
|
||||
agent_view: { agent_description: "Work with a Paperclip agent in a task-backed conversation." },
|
||||
@@ -60,6 +60,7 @@ export const slackRegistrationStateSchema = z.object({
|
||||
status: slackRegistrationStatusSchema,
|
||||
appId: z.string().nullable().optional(),
|
||||
managementUrl: z.string().url().nullable().optional(),
|
||||
managerGrantId: z.string().uuid().nullable().optional(),
|
||||
errorCode: z.string().nullable().optional(),
|
||||
}).strict();
|
||||
export type SlackRegistrationState = z.infer<typeof slackRegistrationStateSchema>;
|
||||
@@ -79,11 +80,33 @@ export const slackAccountStateSchema = z.object({
|
||||
}).strict();
|
||||
export type SlackAccountState = z.infer<typeof slackAccountStateSchema>;
|
||||
export const slackInstallAuthorizationSchema = z.object({ authorizationUrl: z.string().url(), expiresAt: z.string().datetime() });
|
||||
export const slackManagerAuthorizationSchema = slackInstallAuthorizationSchema.extend({ handoff: z.object({ kind: z.literal("paperclip_cloud"), session: z.string().min(16).max(512) }).optional() });
|
||||
export type SlackManagerAuthorization = z.infer<typeof slackManagerAuthorizationSchema>;
|
||||
export type SlackInstallAuthorization = z.infer<typeof slackInstallAuthorizationSchema>;
|
||||
export const slackSetupActionSchema = z.object({}).strict();
|
||||
|
||||
export const slackTestMessageSchema = z.object({
|
||||
requestId: z.string().uuid(),
|
||||
slackUserId: z.string().regex(/^[UW][A-Z0-9]+$/).optional(),
|
||||
accountCursor: z.string().min(1).max(512).optional(),
|
||||
}).strict();
|
||||
export type SlackTestMessageInput = z.infer<typeof slackTestMessageSchema>;
|
||||
export const slackTestMessageResultSchema = z.discriminatedUnion("status", [
|
||||
z.object({ status: z.literal("sent"), slackUrl: z.string().url() }).strict(),
|
||||
z.object({ status: z.literal("connection_required"), accounts: z.array(z.object({
|
||||
id: z.string(), name: z.string(),
|
||||
}).strict()), nextCursor: z.string().nullable() }).strict(),
|
||||
z.object({ status: z.literal("connection_pending"), principalId: z.string().uuid(), expiresAt: z.string().datetime() }).strict(),
|
||||
]);
|
||||
export type SlackTestMessageResult = z.infer<typeof slackTestMessageResultSchema>;
|
||||
|
||||
export function slackRegistrationErrorMessage(code: string): string {
|
||||
const messages: Record<string, string> = {
|
||||
slack_managed_unavailable: "Managed Slack setup is unavailable. Try again later or use your own app before creation starts.",
|
||||
slack_manager_reauthorize: "Reconnect your Slack workspace to continue setting up this agent.",
|
||||
slack_approval_pending: "Slack is waiting for your workspace administrator to approve this app.",
|
||||
slack_approval_required: "Your Slack workspace requires approval. Continue in Slack to request it.",
|
||||
slack_approval_denied: "Your Slack administrator declined this installation. Contact them to continue.",
|
||||
slack_configuration_token_invalid: "Slack rejected the app configuration access token. Generate a new access token and try again.",
|
||||
slack_manifest_invalid: "Slack rejected this app configuration. Check the app details or use manual setup.",
|
||||
slack_manifest_update_pending: "Your Slack app is saved, but its event settings still need to be applied. Enter an app configuration access token to retry configuring the same app.",
|
||||
@@ -96,9 +119,29 @@ export function slackRegistrationErrorMessage(code: string): string {
|
||||
slack_install_identity_mismatch: "Slack returned a different app, workspace, or bot. Install the app created for this connection.",
|
||||
slack_install_scopes_missing: "Slack did not grant all required bot permissions. Install the app again and approve its requested permissions.",
|
||||
slack_bot_already_connected: "This Slack bot is already connected to Paperclip. Resume its existing connection or use a different app.",
|
||||
slack_install_token_invalid: "Slack rejected the saved bot credentials. Continue in Slack to authorize this app again.",
|
||||
slack_configuration_incomplete: "The app is installed, but Paperclip could not finish connecting it. Retry connecting the saved installation.",
|
||||
slack_install_account_missing: "Slack did not identify your account. Authorize the same app again to connect your Slack account.",
|
||||
slack_install_account_conflict: "This Slack account already has an existing link. Manage account links in connection settings, or authorize with your own Slack account.",
|
||||
};
|
||||
return messages[code] ?? "Slack setup could not be completed. Resume setup and try again.";
|
||||
}
|
||||
|
||||
/** Persisted legacy values keep their original meaning. Missing means manual. */
|
||||
export type SlackSetupMethod = "managed" | "automatic" | "manual" | "existing";
|
||||
export function isProvisionedSlackSetup(method: string | undefined): boolean {
|
||||
return method === "automatic" || method === "managed";
|
||||
}
|
||||
export const SLACK_MANAGER_SCOPES = ["app_configurations:read", "app_configurations:write", "managed_apps:install"] as const;
|
||||
export const slackSetupOptionsSchema = z.object({
|
||||
managedAvailable: z.boolean(),
|
||||
defaultMethod: z.enum(["managed", "automatic"]),
|
||||
workspaces: z.array(z.object({ grantId: z.string().uuid(), workspaceId: z.string(), workspaceName: z.string(), userId: z.string() })),
|
||||
});
|
||||
export type SlackSetupOptions = z.infer<typeof slackSetupOptionsSchema>;
|
||||
export const slackManagedProvisionSchema = z.object({
|
||||
requestId: z.string().uuid(), grantId: z.string().uuid(), confirmedNoAppCreated: z.boolean().optional(), reconnect: z.boolean().optional(),
|
||||
}).strict();
|
||||
export type SlackManagedProvisionInput = z.infer<typeof slackManagedProvisionSchema>;
|
||||
export const slackManagedResultSchema = z.object({ authorization: slackInstallAuthorizationSchema.optional() });
|
||||
export type SlackManagedResult = z.infer<typeof slackManagedResultSchema>;
|
||||
@@ -190,7 +190,8 @@ export interface SlackAppConfiguration {
|
||||
}
|
||||
|
||||
export interface ChatEndpointSetupState {
|
||||
slackSetupMethod?: "automatic" | "manual" | "existing";
|
||||
slackSetupMethod?: "managed" | "automatic" | "manual" | "existing";
|
||||
slackManagerError?: string;
|
||||
slackRegistration?: import("../slack-app-manifest.js").SlackRegistrationState;
|
||||
/** Provider-confirmed app icon outcome; configuration tokens are never retained. */
|
||||
slackAvatar?: import("../slack-app-manifest.js").SlackAvatarState;
|
||||
@@ -508,10 +509,11 @@ export interface CreateChatEndpointInput {
|
||||
applicationId?: string;
|
||||
name?: string;
|
||||
slackApp?: SlackAppConfiguration;
|
||||
slackSetupMethod?: "managed" | "automatic" | "manual" | "existing";
|
||||
}
|
||||
|
||||
export interface UpdateChatEndpointInput {
|
||||
slackSetupMethod?: "automatic" | "manual" | "existing";
|
||||
slackSetupMethod?: "managed" | "automatic" | "manual" | "existing";
|
||||
slackApp?: SlackAppConfiguration;
|
||||
communicationInstructions?: string;
|
||||
allowDirectMessages?: boolean;
|
||||
|
||||
@@ -80,9 +80,10 @@ export const createChatEndpointSchema = z
|
||||
applicationId: z.string().uuid().optional(),
|
||||
name: z.string().trim().min(1).max(160).optional(),
|
||||
slackApp: slackAppConfigurationSchema.optional(),
|
||||
slackSetupMethod: z.enum(["managed", "automatic", "manual", "existing"]).optional(),
|
||||
})
|
||||
.strict()
|
||||
.refine(value => !value.slackApp || value.provider === "slack", {
|
||||
.refine(value => (!value.slackApp && !value.slackSetupMethod) || value.provider === "slack", {
|
||||
message: "Slack app details only apply to Slack connections", path: ["slackApp"],
|
||||
});
|
||||
|
||||
@@ -90,7 +91,7 @@ export const updateChatEndpointSchema = z
|
||||
.object({
|
||||
communicationInstructions: multilineTextSchema.pipe(z.string().trim().max(4000)).optional(),
|
||||
slackApp: slackAppConfigurationSchema.optional(),
|
||||
slackSetupMethod: z.enum(["automatic", "manual", "existing"]).optional(),
|
||||
slackSetupMethod: z.enum(["managed", "automatic", "manual", "existing"]).optional(),
|
||||
allowDirectMessages: z.boolean().optional(),
|
||||
requireAtMention: z.boolean().optional(),
|
||||
allowGroupChats: z.boolean().optional(),
|
||||
|
||||
@@ -7,7 +7,7 @@ import { resolveCanonicalWorktreeSeedSource } from "./worktree-seed-source.js";
|
||||
const cleanup: string[] = [];
|
||||
|
||||
function makeInstance(prefix: string, instanceId: string) {
|
||||
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
||||
const cwd = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), prefix));
|
||||
cleanup.push(cwd);
|
||||
const configDir = path.join(cwd, ".paperclip");
|
||||
const configPath = path.join(configDir, "config.json");
|
||||
@@ -22,7 +22,7 @@ function makeInstance(prefix: string, instanceId: string) {
|
||||
* names its instance by directory and has no adjacent .env.
|
||||
*/
|
||||
function makeInstanceRoot(instanceId: string) {
|
||||
const home = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-seed-home-"));
|
||||
const home = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-seed-home-"));
|
||||
cleanup.push(home);
|
||||
const configDir = path.join(home, "instances", instanceId);
|
||||
fs.mkdirSync(configDir, { recursive: true });
|
||||
@@ -33,7 +33,7 @@ function makeInstanceRoot(instanceId: string) {
|
||||
|
||||
/** A managed project checkout: a plain clone with no `.paperclip` of its own. */
|
||||
function makePlainCheckout() {
|
||||
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-seed-checkout-"));
|
||||
const cwd = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-seed-checkout-"));
|
||||
cleanup.push(cwd);
|
||||
return cwd;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user