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:
DottaandPaperclip committed 2026-10-12 20:40:11 -05:00
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();
}
});
});
+44 -1
View File
@@ -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>;
+4 -2
View File
@@ -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;
}