Files
PaperClipAI/packages/shared/src/feature-catalog.ts
T
DottaandPaperclip 3ce8d976a9 Add experimental Paperclip Notebook MCP connection (#16984)
## Thinking Path

> - Paperclip connects agents to external tools through saved accounts
and per-action permissions.
> - Notebook exposes artifact management through an ordinary remote MCP
server with OAuth.
> - A private pilot needs a hidden, default-off catalog and execution
gate.
> - Local file publishing belongs to Notebook, which can accept bounded
file bytes through ordinary MCP tools.
> - Paperclip therefore needs the connector registration and generic
availability checks, with no file-transfer architecture.

## Linked Issues or Issue Description

**Subsystem affected**

Apps catalog, experimental feature metadata, connection setup, and
connector availability.

**Problem or motivation**

Users need to connect Notebook as their Cloud account, assign its tools
to agents, and retain Paperclip's normal Allowed / Ask first / Off
rules. The private pilot must remain hidden until an operator enables
it. Notebook should own its upload protocol and local publishing script.

**Proposed solution**

Register a personal remote MCP OAuth connection at Notebook's canonical
endpoint. Reuse existing OAuth discovery, credential storage, refresh,
reconnect, and removal. Enforce availability through generic catalog
feature metadata and saved application identity. Keep Notebook's
standalone uploader in its own service repository; it calls the ordinary
gateway and receives no saved OAuth credential.

**Alternatives considered**

A gallery-only filter permits retained tools and deep links while
disabled. A shared workspace publisher and private upload-ticket
transport add runtime behavior that a normal MCP connector does not
require. Notebook can receive file bytes directly through its own tools
instead.

**Roadmap alignment**

This extends Connected Apps and supports Artifacts and Work Products.
ROADMAP.md and related PRs were checked. This PR is standalone on
master. The separate OAuth lifecycle and file-transfer proposals (#17085
and #17086) are closed and are not dependencies.

## What Changed

- Register Notebook's personal MCP OAuth method, canonical endpoint,
scopes, catalog metadata, and branding.
- Declare managed `enablePaperclipNotebook` with false defaults and no
user-facing settings toggle or staging opt-in.
- Use generic availability metadata for gallery/setup, retained
connection identity, OAuth completion, health, tool/context discovery,
and dispatch. Resources/prompts recheck before credentials, including
retained handles. Removal remains available while disabled.
- Resolve catalog application identity from the saved application key,
so editable connection labels cannot bypass the hidden gate.
- Select a method's sole supported credential kind when no kind is
supplied.
- Keep UI changes to metadata-driven availability and existing
setup/reconnect controls.
- Document Notebook's service-owned upload script, inline publication,
ordinary action policies, and credential behavior.
- Add ordinary MCP OAuth, assignment, action-policy, disabled-state,
settings, and catalog tests.

No workspace contribution registry, private transfer endpoint, native
publisher, Runner file-reader change, or Paperclip CLI upload command is
included. The entire native-runtime and CLI trees match master. Upload
code lives in `paperclip-cloud/notebook/`.

## Verification

Source head: `65ef741e4f476c67425b57d2fb2d6d4c00e99b06`, directly on
master `53732c811bb69655eb43d3d12f2063ae4ca5ad67`. The diff is 42 files.
Shared execution changes only enforce generic catalog availability; tool
access also resolves saved application identity and the sole supported
credential kind. Neither module contains Notebook upload logic.

- Catalog/shared/settings/UI suites: 592 tests passed across 12 files.
Shared/server/UI typechecks passed using installed compilers.
- Fresh real PostgreSQL generic MCP suite: 84/84 passed at final source;
server typecheck and module boundaries pass. Coverage includes personal
OAuth, ordinary refresh/reconnect, assignments, Allowed / Ask first /
Off, hidden feature enforcement, and local credential removal. Four new
resources/prompts cases failed against the prior code and pass after
filtering saved application identity and rechecking before credentials.
They prove missing/relabelled setup markers and stale eligibility cannot
dispatch or read secrets while disabled; an ordinary provider remains
available with the flag off.
- The full tool-access service suite passed 463/464 cases. One unchanged
Google Workspace `slides.write` case timed out at 15 seconds; it passed
on one isolated retry without source or timeout changes. Its exact test
block matches master. The cause is not established; final-head CI must
qualify the complete suite.
- Maps, module boundaries, token gates, and whitespace checks passed.
Byte comparisons confirm the former transfer/runtime/CLI surfaces match
master.
- Fresh exact-head Greptile is 5/5 with zero unresolved findings after
an exhaustive feedback audit. Current-head CI workflow
[39625691137](https://github.com/paperclipai/paperclip/actions/runs/39625691137)
succeeded, including both required checks (`ci / verify`, `ci / e2e`),
Build, and browser E2E. Supplementary security/advisory scan status is
recorded in the PR Checks panel and qualification receipt; prior-head
scans are not counted. The superseded `4bd7af9b2e` completed CI
successfully, but Greptile found the valid resources/prompts
availability gap described above. It was fixed, replied to, and
resolved; earlier-head results do not qualify this revision.

Preserved environment failures: the first socket-restricted UI/route run
had 93 listener setup failures, and the same 93 cases passed with
local-socket access. The package-manager launcher could not fetch its
signature metadata; installed compilers passed without dependency or
configuration changes. The full service timeout and isolated retry
remain recorded separately. The superseded `585e870e73` CI also failed
an unchanged agent-chat project-plan revision assertion after task
creation; the complete spec and relevant runtime sources match master,
and a same-master prior run passed. Its cause is not established. Logs
are preserved; no test or timeout was weakened and no old-head rerun was
requested. The subsequent heads run full CI independently; no prior-head
pass substitutes for current qualification.

The Notebook uploader requires a separate service release. This public
PR adds no uploader runtime to Paperclip and claims no newly deployed
browser/agent acceptance. The Notebook-owned upload path is qualified
separately. Code-owner review remains required before merge.

## Risks

- The feature flag gates the curated connector; it does not create a
Notebook service-side team allowlist or block arbitrary standard MCP
URLs.
- Publishing, sharing, and deletion retain the existing assignment and
action-policy boundaries.
- The hidden feature must be checked for retained tools and saved
identity, including when connection labels change.
- Disconnect removes local credentials. It does not revoke the remote
Notebook authorization; the owner can revoke it in Notebook.
- Ordinary refresh uses the existing client. A lost response after
refresh-token rotation can require reconnect.
- The standalone Notebook script and its bounded upload tools must ship
together in the Notebook service before that documented convenience path
is available.
- No database schema, workflow, lockfile, shared transfer, or Runner
change is included. No merge, deployment, or rollout is performed by
this update.

## Model Used

OpenAI GPT-6 in Codex, with reasoning, repository inspection, code
execution, and tool use. The runtime did not expose a more specific
model ID or context-window size.

## 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>
2026-10-21 11:07:18 -05:00

426 lines
16 KiB
TypeScript

import { z } from "zod";
import { instanceExperimentalSettingsSchema } from "./validators/instance.js";
/**
* Feature catalog for cloud-managed instances.
*
* The instance-settings zod schema is the feature manifest; this module adds
* only metadata about the flags the schema already declares. Keys are derived
* from the schema type, so adding, removing, or renaming a boolean flag in
* `instanceExperimentalSettingsSchema` without updating the metadata map is a
* compile error (and vice versa).
*
* Tiers:
* - `preference`: tenant-controllable taste setting; the cloud harness does
* not manage it.
* - `managed`: the cloud harness may set this per fleet/stack via
* `PAPERCLIP_MANAGED_CONFIG`.
* - `floor`: pinned by code on managed instances; no flag value may widen it.
*/
export const FEATURE_TIERS = ["preference", "managed", "floor"] as const;
export type FeatureTier = (typeof FEATURE_TIERS)[number];
type ExperimentalSettings = z.infer<typeof instanceExperimentalSettingsSchema>;
/**
* The boolean flag keys of the experimental settings schema. Non-flag keys
* (activation timestamps, numeric tuning values) are excluded.
*/
export type InstanceFeatureKey = {
[K in keyof ExperimentalSettings]: ExperimentalSettings[K] extends boolean ? K : never;
}[keyof ExperimentalSettings];
export interface FeatureCatalogEntry {
title: string;
description: string;
tier: FeatureTier;
/** Desired default on cloud-managed instances. */
cloudDefault: boolean;
/** Must match the schema default; enforced by test. */
selfHostedDefault: boolean;
}
export const INSTANCE_FEATURE_CATALOG: Record<InstanceFeatureKey, FeatureCatalogEntry> = {
enableEnvironments: {
title: "Environments",
description:
"Show environment management in company settings and allow project and agent environment assignment controls.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableAiConnectionRouters: {
title: "AI connection routers",
description: "Allow experimental plugin connections to choose task-pinned accounts and harnesses.",
tier: "managed", cloudDefault: false, selfHostedDefault: false,
},
enableNativeRunner: {
title: "Paperclip Runner",
description:
"Allow explicitly configured local Codex, OpenCode, and qualified ACPX agents to use the experimental Rust Paperclip Runner, including authenticated sandbox ingress when required. Onboarding remains on legacy adapters.",
tier: "managed",
cloudDefault: false,
// On by default for self-hosted instances. Requires a Rust toolchain (or
// PAPERCLIP_RUNNER_BINARY) for `pnpm dev`, which builds runnerd whenever
// this is on.
selfHostedDefault: true,
},
enableManagedSandboxOnly: {
title: "Managed Environment Only",
description:
"Hide the local environment and run all agents in the platform-managed environment.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableIsolatedWorkspaces: {
title: "Isolated Workspaces",
description:
"Show execution workspace controls in project configuration and allow isolated workspace behavior for task runs.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableIsolatedWorkspacesByDefault: {
title: "Isolated Workspaces By Default",
description:
"Treat a project that has no execution workspace policy of its own as if it selected isolated workspaces, so its tasks get a per-task worktree instead of sharing the project checkout. Requires Isolated Workspaces. A project that carries its own policy keeps it.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableStreamlinedLeftNavigation: {
title: "Streamlined Left Navigation",
description: "Use the streamlined main sidebar navigation layout.",
tier: "preference",
cloudDefault: true,
selfHostedDefault: true,
},
enableStreamlinedUi: {
title: "Streamlined UI",
description:
"Use the streamlined application shell, shared task collections, focused task detail layout, contextual navigation, and simplified main sidebar.",
tier: "preference",
cloudDefault: true,
selfHostedDefault: true,
},
enableApps: {
title: "Apps (compatibility)",
description:
"Deprecated compatibility key. Apps is always enabled; stored and managed values are ignored.",
tier: "managed",
cloudDefault: true,
selfHostedDefault: true,
},
enablePublicMcp: {
title: "Assistant connections (MCP)",
description: "Let external assistants connect as a person to review work, delegate tasks, add feedback, and follow results.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableGitHubReviewBots: {
title: "GitHub review bots",
description: "Show GitHub review bot setup and management independently of chat connectors. Existing bots keep running when hidden; GitHub tool connections are unaffected.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableOpenAiDot: {
title: "OpenAI Dot",
description: "Add OpenAI Dot as a standalone agent choice with its own experimental opt-in. Requires Assistant connections (MCP) and an authenticated instance with a public HTTPS URL.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableGrokBot: {
title: "Grok Bot",
description: "Connect an existing Grok Bot as an experimental Paperclip agent. Requires Assistant connections (MCP), Paperclip Runner, and an authenticated public HTTPS instance. Provider spending is not reported.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableMuse: {
title: "Muse — Personal agent",
description: "Connect your personal Meta Muse through its managed background receiver and the external Paperclip Runner. Experimental; requires authenticated public HTTPS and Paperclip Runner.",
tier: "managed", cloudDefault: false, selfHostedDefault: false,
},
enableChatConnectors: {
title: "Chat connectors",
description:
"Show experimental chat connector setup and Board surfaces. Existing connections keep running when hidden; AgentMail, GitHub review bots, and tool connectors are unaffected.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableMemoryConnectors: {
title: "Memory connectors",
description: "Show experimental Mem0, Zep, Supermemory, Cognee, and Honcho setup. Existing connections keep running when hidden.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enablePaperclipNotebook: {
title: "Paperclip Notebook",
description: "Allow Paperclip Notebook connections and publishing through an explicitly enabled instance rollout.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableMcpAggregators: {
title: "MCP aggregators (compatibility)",
description: "Deprecated compatibility key. MCP aggregators are always enabled; stored and managed values are ignored.",
tier: "managed",
cloudDefault: true,
selfHostedDefault: true,
},
enableCases: {
title: "Cases",
description:
"Durable work products that tasks create and iterate on. Adds the Cases tab and the agent case API.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableAgentChat: {
title: "Agent Chat",
description:
"Persistent task-backed conversations that clarify goals and hand work off to tasks. Chat leads the Work group with an agent rail, and each chat's side panel shows the agent's tasks and artifacts as cards.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableCombinedInboxTasks: {
title: "Combined Inbox + Task List",
description:
"Inbox becomes a set of views inside Tasks: one Tasks row in the nav carries the unread badge, and a Views menu reaches every inbox and task view.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableConferenceRoomChat: {
title: "Conference Room Chat",
description:
"Add the Conference Room team chat, the live activity feed, and the redesigned onboarding; restyles task threads as chat bubbles.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableClassicTaskInterface: {
title: "Classic Task Interface",
description:
"Restore the pre-chat task detail page: the page-level header with inline description editor, the plain comment thread, and the fixed Properties sidebar. Chat-only features (streaming activity folding, inline plan/question cards, the three-mode composer) are unavailable in the classic view.",
tier: "preference",
cloudDefault: false,
selfHostedDefault: false,
},
enableIssuePlanDecompositions: {
title: "Task Plan Decomposition",
description: "Show accepted-plan decomposition history on task detail pages.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableExperimentalFileViewer: {
title: "Experimental File Viewer",
description:
"Show task detail controls for browsing and previewing workspace files relative to a task.",
tier: "preference",
cloudDefault: false,
selfHostedDefault: false,
},
enableStatusCards: {
title: "Status Cards",
description:
"Enable the experimental shared status-card board, update engine, and gated API.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableExternalObjects: {
title: "External Objects",
description:
"Detect external URLs in issues and show resolved status for pull requests, tickets, and other referenced work objects.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableSmokeLab: {
title: "Smoke Lab",
description:
"Add the Smoke Lab tab and dashboard card for exercising integration paths against deterministic local fixtures. Private deployments only.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableBuiltInAgents: {
title: "Built-in Agents",
description:
"Show Paperclip-managed built-in agent surfaces, including roster badges, the Built-in agents tab, and setup controls.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableBetaSkills: {
title: "Beta skills",
description: "Allow agents to pin beta releases of the Paperclip core skill.",
tier: "preference",
cloudDefault: false,
selfHostedDefault: false,
},
enableSummaries: {
title: "Summaries",
description:
"Show Summarizer-generated status slots on project and workspace pages, with on-demand refresh and revision history.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableDecisions: {
title: "Decisions",
description:
"Show the Decisions item in the main sidebar — the attention home that surfaces tasks awaiting input.",
tier: "preference",
cloudDefault: false,
selfHostedDefault: false,
},
enableFastResponses: {
title: "Experimental fast responses",
description:
"Allow short acknowledgements while agents begin work. Configure them in Settings → Connections.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableGoalsSidebarLink: {
title: "Goals Sidebar Link",
description: "Restore the Goals item in the main sidebar while the goals surface is being evaluated.",
tier: "preference",
cloudDefault: false,
selfHostedDefault: false,
},
enableSimplifiedEnglishInteractions: {
title: "Simplified English Interactions",
description:
"Instruct agents to write user interactions (confirmations, questions, suggested tasks) in ASD-STE100 Simplified Technical English with brief decision context.",
tier: "preference",
cloudDefault: false,
selfHostedDefault: false,
},
enableServerInfoDebugView: {
title: "Server Info Debug View",
description:
"Show a Server section in the account drawer with the current server restart time and running commit.",
tier: "preference",
cloudDefault: false,
selfHostedDefault: false,
},
enablePaperclipDeveloperMode: {
title: "Paperclip Developer Mode",
description:
"Show internal Paperclip maintainer tools and observability links, including Honeycomb trace queries on run pages.",
tier: "preference",
cloudDefault: false,
selfHostedDefault: false,
},
autoRestartDevServerWhenIdle: {
title: "Auto-Restart Dev Server When Idle",
description:
"In local development, wait for queued and running agent runs to finish, then restart the server automatically when backend changes make the current boot stale.",
tier: "preference",
cloudDefault: false,
selfHostedDefault: false,
},
enableWorkspaceBranchReconcileForward: {
title: "Workspace Branch Reconcile Forward",
description:
"Let execution workspaces reconcile a diverged recorded branch forward instead of failing branch containment.",
tier: "managed",
cloudDefault: true,
selfHostedDefault: true,
},
enableWorkspaceDirtyQuarantineRepair: {
title: "Workspace Dirty Quarantine Repair",
description:
"Let workspace runtime recovery quarantine and repair dirty execution workspaces before runs.",
tier: "managed",
cloudDefault: true,
selfHostedDefault: true,
},
enableOwnerInstanceAdmin: {
title: "Owner Instance Admin",
description:
"On cloud-managed instances, grant the stack owner instance-admin access to their own dedicated instance. Elevation is computed at the trusted-header auth boundary; no instance admin role rows are created. Inert on self-hosted instances.",
tier: "managed",
cloudDefault: true,
selfHostedDefault: false,
},
enableSandboxDuplexBridge: {
title: "Sandbox Duplex Bridge",
description:
"Let a run open the sandbox duplex command-stream bridge when the provider grants the capability. The host reads this per run before it selects the transport. Off keeps the file bridge for every run.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableRunnerPreviewIngress: {
title: "Runner Preview Ingress (Deprecated)",
description:
"Compatibility-only key retained for older managed configs. Runner ingress follows the Paperclip Runner setting.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableWorktreeRunExecution: {
title: "Worktree Run Execution",
description:
"Let the scheduler execute runs inside an isolated git-worktree preview instance for tasks created after activation.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableFirstTaskPlanProposal: {
title: "First task: propose with a plan document",
description:
"When the user's first request is a single task, the chief of staff writes a short plan document and a checkbox card instead of a one-card confirmation. Applies to organizations created after the toggle is flipped.",
tier: "preference",
cloudDefault: false,
selfHostedDefault: false,
},
};
export const INSTANCE_FEATURE_KEYS = Object.keys(INSTANCE_FEATURE_CATALOG).sort() as InstanceFeatureKey[];
/**
* Shape of the `feature-catalog.json` release artifact the cloud harness
* imports per app release and validates feature writes against.
*/
export const featureCatalogArtifactSchema = z
.object({
catalogVersion: z.string().min(1),
features: z.record(
z.string().min(1),
z.object({ tier: z.enum(FEATURE_TIERS) }).strict(),
),
})
.strict();
export type FeatureCatalogArtifact = z.infer<typeof featureCatalogArtifactSchema>;
export function buildFeatureCatalogArtifact(catalogVersion: string): FeatureCatalogArtifact {
if (catalogVersion.trim().length === 0) {
throw new Error("catalogVersion must be a non-empty string");
}
const features: FeatureCatalogArtifact["features"] = {};
for (const key of INSTANCE_FEATURE_KEYS) {
features[key] = { tier: INSTANCE_FEATURE_CATALOG[key].tier };
}
return { catalogVersion, features };
}
/** Deterministic serialization (sorted keys, trailing newline) for the artifact file. */
export function renderFeatureCatalogArtifact(catalogVersion: string): string {
return `${JSON.stringify(buildFeatureCatalogArtifact(catalogVersion), null, 2)}\n`;
}