feat(adapters): add capability flags to ServerAdapterModule

Replace 5 hardcoded adapter type lists with declarative capability flags
on ServerAdapterModule, enabling external adapter plugins to declare
their capabilities without modifying Paperclip source.

New optional fields on ServerAdapterModule:
- supportsInstructionsBundle: managed instructions bundle support
- instructionsPathKey: config key for instructions file path
- requiresMaterializedRuntimeSkills: skill materialization needed

Server changes:
- agents.ts: capability-aware helpers with legacy fallbacks
- adapters.ts: expose capabilities in GET /api/adapters response
- registry.ts: explicit flags on all built-in adapters

UI changes:
- New useAdapterCapabilities hook for capability lookups
- AgentDetail.tsx: replace hardcoded isLocal allowlist
- AgentConfigForm.tsx: replace NONLOCAL_TYPES denylist
- OnboardingWizard.tsx: replace NONLOCAL_TYPES denylist

All flags are optional with backwards-compatible fallbacks to the
legacy hardcoded lists for adapters that don't set them.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-04-13 01:02:13 +00:00
parent b649bd454f
commit 904e9cb95e
9 changed files with 153 additions and 15 deletions
@@ -0,0 +1,37 @@
import { useMemo } from "react";
import { useQuery } from "@tanstack/react-query";
import { adaptersApi, type AdapterCapabilities } from "@/api/adapters";
import { queryKeys } from "@/lib/queryKeys";
/**
* Returns a lookup function that resolves adapter capabilities by type.
*
* Capabilities are fetched from the server adapter listing API and cached
* via react-query. When the data is not yet loaded, the lookup returns
* a conservative default (all capabilities false).
*/
export function useAdapterCapabilities(): (type: string) => AdapterCapabilities {
const { data: adapters } = useQuery({
queryKey: queryKeys.adapters.all,
queryFn: () => adaptersApi.list(),
staleTime: 5 * 60 * 1000,
});
const capMap = useMemo(() => {
const map = new Map<string, AdapterCapabilities>();
if (adapters) {
for (const a of adapters) {
map.set(a.type, a.capabilities);
}
}
return map;
}, [adapters]);
return (type: string): AdapterCapabilities =>
capMap.get(type) ?? {
supportsInstructionsBundle: false,
supportsSkills: false,
supportsLocalAgentJwt: false,
requiresMaterializedRuntimeSkills: false,
};
}
+8
View File
@@ -4,6 +4,13 @@
import { api } from "./client";
export interface AdapterCapabilities {
supportsInstructionsBundle: boolean;
supportsSkills: boolean;
supportsLocalAgentJwt: boolean;
requiresMaterializedRuntimeSkills: boolean;
}
export interface AdapterInfo {
type: string;
label: string;
@@ -11,6 +18,7 @@ export interface AdapterInfo {
modelsCount: number;
loaded: boolean;
disabled: boolean;
capabilities: AdapterCapabilities;
/** Installed version (for external npm adapters) */
version?: string;
/** Package name (for external adapters) */
+4 -2
View File
@@ -50,6 +50,7 @@ import { listAdapterOptions, listVisibleAdapterTypes } from "../adapters/metadat
import { getAdapterLabel } from "../adapters/adapter-display-registry";
import { useDisabledAdaptersSync } from "../adapters/use-disabled-adapters";
import { buildAgentUpdatePatch, type AgentConfigOverlay } from "../lib/agent-config-patch";
import { useAdapterCapabilities } from "../adapters/use-adapter-capabilities";
/* ---- Create mode values ---- */
@@ -269,8 +270,9 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
const adapterType = isCreate
? props.values.adapterType
: overlay.adapterType ?? props.agent.adapterType;
const NONLOCAL_TYPES = new Set(["process", "http", "openclaw_gateway"]);
const isLocal = !NONLOCAL_TYPES.has(adapterType);
const getCapabilities = useAdapterCapabilities();
const adapterCaps = getCapabilities(adapterType);
const isLocal = adapterCaps.supportsInstructionsBundle || adapterCaps.supportsSkills || adapterCaps.supportsLocalAgentJwt;
const showLegacyWorkingDirectoryField =
isLocal && shouldShowLegacyWorkingDirectoryField({ isCreate, adapterConfig: config });
+4 -2
View File
@@ -25,6 +25,7 @@ import {
import { getUIAdapter } from "../adapters";
import { listUIAdapters } from "../adapters";
import { useDisabledAdaptersSync } from "../adapters/use-disabled-adapters";
import { useAdapterCapabilities } from "../adapters/use-adapter-capabilities";
import { getAdapterDisplay } from "../adapters/adapter-display-registry";
import { defaultCreateValues } from "./agent-config-defaults";
import { parseOnboardingGoalInput } from "../lib/onboarding-goal";
@@ -198,8 +199,9 @@ export function OnboardingWizard() {
queryFn: () => agentsApi.adapterModels(createdCompanyId!, adapterType),
enabled: Boolean(createdCompanyId) && effectiveOnboardingOpen && step === 2
});
const NONLOCAL_TYPES = new Set(["process", "http", "openclaw_gateway"]);
const isLocalAdapter = !NONLOCAL_TYPES.has(adapterType);
const getCapabilities = useAdapterCapabilities();
const adapterCaps = getCapabilities(adapterType);
const isLocalAdapter = adapterCaps.supportsInstructionsBundle || adapterCaps.supportsSkills || adapterCaps.supportsLocalAgentJwt;
// Build adapter grids dynamically from the UI registry + display metadata.
// External/plugin adapters automatically appear with generic defaults.
+3 -7
View File
@@ -26,6 +26,7 @@ import { AgentConfigForm } from "../components/AgentConfigForm";
import { PageTabBar } from "../components/PageTabBar";
import { adapterLabels, roleLabels, help } from "../components/agent-config-primitives";
import { ToggleSwitch } from "@/components/ui/toggle-switch";
import { useAdapterCapabilities } from "@/adapters/use-adapter-capabilities";
import { MarkdownEditor } from "../components/MarkdownEditor";
import { assetsApi } from "../api/assets";
import { getUIAdapter, buildTranscript, onAdapterChange } from "../adapters";
@@ -1719,13 +1720,8 @@ function PromptsTab({
externalBundleRef.current = null;
}, [agent.id]);
const isLocal =
agent.adapterType === "claude_local" ||
agent.adapterType === "codex_local" ||
agent.adapterType === "opencode_local" ||
agent.adapterType === "pi_local" ||
agent.adapterType === "hermes_local" ||
agent.adapterType === "cursor";
const getCapabilities = useAdapterCapabilities();
const isLocal = getCapabilities(agent.adapterType).supportsInstructionsBundle;
const { data: bundle, isLoading: bundleLoading } = useQuery({
queryKey: queryKeys.agents.instructionsBundle(agent.id),