diff --git a/docs/adapters/creating-an-adapter.md b/docs/adapters/creating-an-adapter.md index ae5e4ccbe5..c81274f755 100644 --- a/docs/adapters/creating-an-adapter.md +++ b/docs/adapters/creating-an-adapter.md @@ -203,6 +203,43 @@ export const sessionCodec: AdapterSessionCodec = { }; ``` +## Capability Flags + +Adapters can declare what "local" capabilities they support by setting optional fields on the `ServerAdapterModule`. The server and UI use these flags to decide which features to enable for agents using the adapter (instructions bundle editor, skills sync, JWT auth, etc.). + +| Flag | Type | Default | What it controls | +|------|------|---------|------------------| +| `supportsLocalAgentJwt` | `boolean` | `false` | Whether heartbeat generates a local JWT for the agent | +| `supportsInstructionsBundle` | `boolean` | `false` | Managed instructions bundle (AGENTS.md) — server-side resolution + UI editor | +| `instructionsPathKey` | `string` | `"instructionsFilePath"` | The `adapterConfig` key that holds the instructions file path | +| `requiresMaterializedRuntimeSkills` | `boolean` | `false` | Whether runtime skill entries must be written to disk before execution | + +These flags are exposed via `GET /api/adapters` in a `capabilities` object, along with a derived `supportsSkills` flag (true when `listSkills` or `syncSkills` is defined). + +### Example + +```ts +export function createServerAdapter(): ServerAdapterModule { + return { + type: "my_k8s_adapter", + execute: myExecute, + testEnvironment: myTestEnvironment, + listSkills: myListSkills, + syncSkills: mySyncSkills, + + // Capability flags + supportsLocalAgentJwt: true, + supportsInstructionsBundle: true, + instructionsPathKey: "instructionsFilePath", + requiresMaterializedRuntimeSkills: true, + }; +} +``` + +With these flags set, the Paperclip UI will automatically show the instructions bundle editor, skills management tab, and working directory field for agents using this adapter — no Paperclip source changes required. + +If capability flags are not set, the server falls back to legacy hardcoded lists for built-in adapter types. External adapters that omit the flags will default to `false` for all capabilities. + ## Skills Injection Make Paperclip skills discoverable to your agent runtime without writing to the agent's working directory: diff --git a/packages/adapter-utils/src/types.ts b/packages/adapter-utils/src/types.ts index 534a5a5975..e02b9efe8d 100644 --- a/packages/adapter-utils/src/types.ts +++ b/packages/adapter-utils/src/types.ts @@ -328,6 +328,36 @@ export interface ServerAdapterModule { * resolved inside this method — the caller receives a fully hydrated schema. */ getConfigSchema?: () => Promise | AdapterConfigSchema; + + // --------------------------------------------------------------------------- + // Adapter capability flags + // + // These allow adapter plugins to declare what "local" capabilities they + // support, replacing hardcoded type lists in the server and UI. + // All flags are optional — when undefined, the server falls back to + // legacy hardcoded lists for built-in adapters. + // --------------------------------------------------------------------------- + + /** + * Adapter supports managed instructions bundle (AGENTS.md files). + * When true, the server uses instructionsPathKey (default "instructionsFilePath") + * to resolve the instructions config key, and the UI shows the bundle editor. + * Built-in local adapters default to true; external plugins must opt in. + */ + supportsInstructionsBundle?: boolean; + + /** + * The adapterConfig key that holds the instructions file path. + * Defaults to "instructionsFilePath" when supportsInstructionsBundle is true. + */ + instructionsPathKey?: string; + + /** + * Adapter needs runtime skill entries materialized (written to disk) + * before being passed via config. Used by adapters that scan a directory + * rather than reading config.paperclipRuntimeSkills. + */ + requiresMaterializedRuntimeSkills?: boolean; } // --------------------------------------------------------------------------- diff --git a/server/src/__tests__/adapter-registry.test.ts b/server/src/__tests__/adapter-registry.test.ts index 6f7b097399..4e473df5b1 100644 --- a/server/src/__tests__/adapter-registry.test.ts +++ b/server/src/__tests__/adapter-registry.test.ts @@ -95,6 +95,51 @@ describe("server adapter registry", () => { ]); }); + it("exposes capability flags from registered adapters", () => { + const adapterWithCaps: ServerAdapterModule = { + type: "external_test", + execute: async () => ({ exitCode: 0, signal: null, timedOut: false }), + testEnvironment: async () => ({ + adapterType: "external_test", + status: "pass" as const, + checks: [], + testedAt: new Date(0).toISOString(), + }), + supportsLocalAgentJwt: true, + supportsInstructionsBundle: true, + instructionsPathKey: "customPathKey", + requiresMaterializedRuntimeSkills: true, + }; + + registerServerAdapter(adapterWithCaps); + + const resolved = findActiveServerAdapter("external_test"); + expect(resolved).not.toBeNull(); + expect(resolved!.supportsInstructionsBundle).toBe(true); + expect(resolved!.instructionsPathKey).toBe("customPathKey"); + expect(resolved!.requiresMaterializedRuntimeSkills).toBe(true); + expect(resolved!.supportsLocalAgentJwt).toBe(true); + }); + + it("returns undefined for capability flags on adapters that do not set them", () => { + registerServerAdapter(externalAdapter); + + const resolved = findActiveServerAdapter("external_test"); + expect(resolved).not.toBeNull(); + expect(resolved!.supportsInstructionsBundle).toBeUndefined(); + expect(resolved!.instructionsPathKey).toBeUndefined(); + expect(resolved!.requiresMaterializedRuntimeSkills).toBeUndefined(); + }); + + it("built-in claude_local adapter declares capability flags", () => { + const adapter = findActiveServerAdapter("claude_local"); + expect(adapter).not.toBeNull(); + expect(adapter!.supportsInstructionsBundle).toBe(true); + expect(adapter!.instructionsPathKey).toBe("instructionsFilePath"); + expect(adapter!.requiresMaterializedRuntimeSkills).toBe(false); + expect(adapter!.supportsLocalAgentJwt).toBe(true); + }); + it("switches active adapter behavior back to the builtin when an override is paused", async () => { const builtIn = findServerAdapter("claude_local"); expect(builtIn).not.toBeNull(); diff --git a/server/src/__tests__/adapter-routes.test.ts b/server/src/__tests__/adapter-routes.test.ts index 32bf991dd1..31168aecd3 100644 --- a/server/src/__tests__/adapter-routes.test.ts +++ b/server/src/__tests__/adapter-routes.test.ts @@ -77,6 +77,75 @@ describe("adapter routes", () => { unregisterServerAdapter("claude_local"); }); + it("GET /api/adapters includes capabilities object for each adapter", async () => { + const app = createApp(); + + const res = await request(app).get("/api/adapters"); + expect(res.status).toBe(200); + expect(Array.isArray(res.body)).toBe(true); + expect(res.body.length).toBeGreaterThan(0); + + // Every adapter should have a capabilities object + for (const adapter of res.body) { + expect(adapter.capabilities).toBeDefined(); + expect(typeof adapter.capabilities.supportsInstructionsBundle).toBe("boolean"); + expect(typeof adapter.capabilities.supportsSkills).toBe("boolean"); + expect(typeof adapter.capabilities.supportsLocalAgentJwt).toBe("boolean"); + expect(typeof adapter.capabilities.requiresMaterializedRuntimeSkills).toBe("boolean"); + } + }); + + it("GET /api/adapters returns correct capabilities for built-in adapters", async () => { + const app = createApp(); + + const res = await request(app).get("/api/adapters"); + expect(res.status).toBe(200); + + // codex_local has instructions bundle + skills + jwt, no materialized skills + // (claude_local is overridden by beforeEach, so check codex_local instead) + const codexLocal = res.body.find((a: any) => a.type === "codex_local"); + expect(codexLocal).toBeDefined(); + expect(codexLocal.capabilities).toMatchObject({ + supportsInstructionsBundle: true, + supportsSkills: true, + supportsLocalAgentJwt: true, + requiresMaterializedRuntimeSkills: false, + }); + + // process adapter should have no local capabilities + const processAdapter = res.body.find((a: any) => a.type === "process"); + expect(processAdapter).toBeDefined(); + expect(processAdapter.capabilities).toMatchObject({ + supportsInstructionsBundle: false, + supportsSkills: false, + supportsLocalAgentJwt: false, + requiresMaterializedRuntimeSkills: false, + }); + + // cursor adapter should require materialized runtime skills + const cursorAdapter = res.body.find((a: any) => a.type === "cursor"); + expect(cursorAdapter).toBeDefined(); + expect(cursorAdapter.capabilities.requiresMaterializedRuntimeSkills).toBe(true); + expect(cursorAdapter.capabilities.supportsInstructionsBundle).toBe(true); + }); + + it("GET /api/adapters derives supportsSkills from listSkills/syncSkills presence", async () => { + const app = createApp(); + + const res = await request(app).get("/api/adapters"); + expect(res.status).toBe(200); + + // http adapter has no listSkills/syncSkills + const httpAdapter = res.body.find((a: any) => a.type === "http"); + expect(httpAdapter).toBeDefined(); + expect(httpAdapter.capabilities.supportsSkills).toBe(false); + + // codex_local has listSkills/syncSkills + const codexLocal = res.body.find((a: any) => a.type === "codex_local"); + expect(codexLocal).toBeDefined(); + expect(codexLocal.capabilities.supportsSkills).toBe(true); + }); + it("uses the active adapter when resolving config schema for a paused builtin override", async () => { const app = createApp(); diff --git a/server/src/adapters/registry.ts b/server/src/adapters/registry.ts index f674622721..f6a1afca1e 100644 --- a/server/src/adapters/registry.ts +++ b/server/src/adapters/registry.ts @@ -97,6 +97,9 @@ const claudeLocalAdapter: ServerAdapterModule = { models: claudeModels, listModels: listClaudeModels, supportsLocalAgentJwt: true, + supportsInstructionsBundle: true, + instructionsPathKey: "instructionsFilePath", + requiresMaterializedRuntimeSkills: false, agentConfigurationDoc: claudeAgentConfigurationDoc, getQuotaWindows: claudeGetQuotaWindows, }; @@ -112,6 +115,9 @@ const codexLocalAdapter: ServerAdapterModule = { models: codexModels, listModels: listCodexModels, supportsLocalAgentJwt: true, + supportsInstructionsBundle: true, + instructionsPathKey: "instructionsFilePath", + requiresMaterializedRuntimeSkills: false, agentConfigurationDoc: codexAgentConfigurationDoc, getQuotaWindows: codexGetQuotaWindows, }; @@ -127,6 +133,9 @@ const cursorLocalAdapter: ServerAdapterModule = { models: cursorModels, listModels: listCursorModels, supportsLocalAgentJwt: true, + supportsInstructionsBundle: true, + instructionsPathKey: "instructionsFilePath", + requiresMaterializedRuntimeSkills: true, agentConfigurationDoc: cursorAgentConfigurationDoc, }; @@ -140,6 +149,9 @@ const geminiLocalAdapter: ServerAdapterModule = { sessionManagement: getAdapterSessionManagement("gemini_local") ?? undefined, models: geminiModels, supportsLocalAgentJwt: true, + supportsInstructionsBundle: true, + instructionsPathKey: "instructionsFilePath", + requiresMaterializedRuntimeSkills: true, agentConfigurationDoc: geminiAgentConfigurationDoc, }; @@ -149,6 +161,8 @@ const openclawGatewayAdapter: ServerAdapterModule = { testEnvironment: openclawGatewayTestEnvironment, models: openclawGatewayModels, supportsLocalAgentJwt: false, + supportsInstructionsBundle: false, + requiresMaterializedRuntimeSkills: false, agentConfigurationDoc: openclawGatewayAgentConfigurationDoc, }; @@ -163,6 +177,9 @@ const openCodeLocalAdapter: ServerAdapterModule = { sessionManagement: getAdapterSessionManagement("opencode_local") ?? undefined, listModels: listOpenCodeModels, supportsLocalAgentJwt: true, + supportsInstructionsBundle: true, + instructionsPathKey: "instructionsFilePath", + requiresMaterializedRuntimeSkills: true, agentConfigurationDoc: openCodeAgentConfigurationDoc, }; @@ -177,6 +194,9 @@ const piLocalAdapter: ServerAdapterModule = { models: [], listModels: listPiModels, supportsLocalAgentJwt: true, + supportsInstructionsBundle: true, + instructionsPathKey: "instructionsFilePath", + requiresMaterializedRuntimeSkills: true, agentConfigurationDoc: piAgentConfigurationDoc, }; @@ -189,6 +209,9 @@ const hermesLocalAdapter: ServerAdapterModule = { syncSkills: hermesSyncSkills, models: hermesModels, supportsLocalAgentJwt: true, + supportsInstructionsBundle: true, + instructionsPathKey: "instructionsFilePath", + requiresMaterializedRuntimeSkills: false, agentConfigurationDoc: hermesAgentConfigurationDoc, detectModel: () => detectModelFromHermes(), }; diff --git a/server/src/routes/adapters.ts b/server/src/routes/adapters.ts index 7a92f7835a..811060adab 100644 --- a/server/src/routes/adapters.ts +++ b/server/src/routes/adapters.ts @@ -59,6 +59,13 @@ interface AdapterInstallRequest { version?: string; } +interface AdapterCapabilities { + supportsInstructionsBundle: boolean; + supportsSkills: boolean; + supportsLocalAgentJwt: boolean; + requiresMaterializedRuntimeSkills: boolean; +} + interface AdapterInfo { type: string; label: string; @@ -66,6 +73,7 @@ interface AdapterInfo { modelsCount: number; loaded: boolean; disabled: boolean; + capabilities: AdapterCapabilities; /** True when an external plugin has replaced a built-in adapter of the same type. */ overriddenBuiltin?: boolean; /** True when the external override for a builtin type is currently paused. */ @@ -103,6 +111,15 @@ function readAdapterPackageVersionFromDisk(record: AdapterPluginRecord): string } } +function buildAdapterCapabilities(adapter: ServerAdapterModule): AdapterCapabilities { + return { + supportsInstructionsBundle: adapter.supportsInstructionsBundle ?? false, + supportsSkills: Boolean(adapter.listSkills || adapter.syncSkills), + supportsLocalAgentJwt: adapter.supportsLocalAgentJwt ?? false, + requiresMaterializedRuntimeSkills: adapter.requiresMaterializedRuntimeSkills ?? false, + }; +} + function buildAdapterInfo(adapter: ServerAdapterModule, externalRecord: AdapterPluginRecord | undefined, disabledSet: Set): AdapterInfo { const fromDisk = externalRecord ? readAdapterPackageVersionFromDisk(externalRecord) : undefined; return { @@ -112,6 +129,7 @@ function buildAdapterInfo(adapter: ServerAdapterModule, externalRecord: AdapterP modelsCount: (adapter.models ?? []).length, loaded: true, // If it's in the registry, it's loaded disabled: disabledSet.has(adapter.type), + capabilities: buildAdapterCapabilities(adapter), overriddenBuiltin: externalRecord ? BUILTIN_ADAPTER_TYPES.has(adapter.type) : undefined, overridePaused: BUILTIN_ADAPTER_TYPES.has(adapter.type) ? isOverridePaused(adapter.type) : undefined, // Prefer on-disk package.json so the UI reflects bumps without relying on store-only fields. diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts index 11b4749064..faf27f24e9 100644 --- a/server/src/routes/agents.ts +++ b/server/src/routes/agents.ts @@ -72,6 +72,8 @@ import { import { getTelemetryClient } from "../telemetry.js"; export function agentRoutes(db: Db) { + // Legacy hardcoded maps — used as fallback when adapter module does not + // declare capability flags explicitly. const DEFAULT_INSTRUCTIONS_PATH_KEYS: Record = { claude_local: "instructionsFilePath", codex_local: "instructionsFilePath", @@ -83,6 +85,22 @@ export function agentRoutes(db: Db) { pi_local: "instructionsFilePath", }; const DEFAULT_MANAGED_INSTRUCTIONS_ADAPTER_TYPES = new Set(Object.keys(DEFAULT_INSTRUCTIONS_PATH_KEYS)); + + /** Check if an adapter supports the managed instructions bundle. */ + function adapterSupportsInstructionsBundle(adapterType: string): boolean { + const adapter = findActiveServerAdapter(adapterType); + if (adapter?.supportsInstructionsBundle !== undefined) return adapter.supportsInstructionsBundle; + return DEFAULT_MANAGED_INSTRUCTIONS_ADAPTER_TYPES.has(adapterType); + } + + /** Resolve the adapter config key for the instructions file path. */ + function resolveInstructionsPathKey(adapterType: string): string | null { + const adapter = findActiveServerAdapter(adapterType); + if (adapter?.instructionsPathKey) return adapter.instructionsPathKey; + if (adapter?.supportsInstructionsBundle === true) return "instructionsFilePath"; + if (adapter?.supportsInstructionsBundle === false) return null; + return DEFAULT_INSTRUCTIONS_PATH_KEYS[adapterType] ?? null; + } const KNOWN_INSTRUCTIONS_PATH_KEYS = new Set(["instructionsFilePath", "agentsMdPath"]); const KNOWN_INSTRUCTIONS_BUNDLE_KEYS = [ "instructionsBundleMode", @@ -557,7 +575,7 @@ export function agentRoutes(db: Db) { adapterType: string; adapterConfig: unknown; }>(agent: T): Promise { - if (!DEFAULT_MANAGED_INSTRUCTIONS_ADAPTER_TYPES.has(agent.adapterType)) { + if (!adapterSupportsInstructionsBundle(agent.adapterType)) { return agent; } @@ -638,7 +656,9 @@ export function agentRoutes(db: Db) { }; } - const ADAPTERS_REQUIRING_MATERIALIZED_RUNTIME_SKILLS = new Set([ + // Legacy hardcoded set — used as fallback when adapter module does not + // declare requiresMaterializedRuntimeSkills explicitly. + const LEGACY_MATERIALIZED_SKILLS_SET = new Set([ "cursor", "gemini_local", "opencode_local", @@ -646,7 +666,11 @@ export function agentRoutes(db: Db) { ]); function shouldMaterializeRuntimeSkillsForAdapter(adapterType: string) { - return ADAPTERS_REQUIRING_MATERIALIZED_RUNTIME_SKILLS.has(adapterType); + const adapter = findActiveServerAdapter(adapterType); + if (adapter?.requiresMaterializedRuntimeSkills !== undefined) { + return adapter.requiresMaterializedRuntimeSkills; + } + return LEGACY_MATERIALIZED_SKILLS_SET.has(adapterType); } async function buildRuntimeSkillConfig( @@ -1617,7 +1641,7 @@ export function agentRoutes(db: Db) { const existingAdapterConfig = asRecord(existing.adapterConfig) ?? {}; const explicitKey = asNonEmptyString(req.body.adapterConfigKey); - const defaultKey = DEFAULT_INSTRUCTIONS_PATH_KEYS[existing.adapterType] ?? null; + const defaultKey = resolveInstructionsPathKey(existing.adapterType); const adapterConfigKey = explicitKey ?? defaultKey; if (!adapterConfigKey) { res.status(422).json({ diff --git a/ui/src/adapters/use-adapter-capabilities.ts b/ui/src/adapters/use-adapter-capabilities.ts new file mode 100644 index 0000000000..281d3adcc0 --- /dev/null +++ b/ui/src/adapters/use-adapter-capabilities.ts @@ -0,0 +1,54 @@ +import { useMemo } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { adaptersApi, type AdapterCapabilities } from "@/api/adapters"; +import { queryKeys } from "@/lib/queryKeys"; + +const ALL_FALSE: AdapterCapabilities = { + supportsInstructionsBundle: false, + supportsSkills: false, + supportsLocalAgentJwt: false, + requiresMaterializedRuntimeSkills: false, +}; + +/** + * Synchronous fallback for known built-in adapter types so capability checks + * return correct values on first render before the /api/adapters call resolves. + */ +const KNOWN_DEFAULTS: Record = { + claude_local: { supportsInstructionsBundle: true, supportsSkills: true, supportsLocalAgentJwt: true, requiresMaterializedRuntimeSkills: false }, + codex_local: { supportsInstructionsBundle: true, supportsSkills: true, supportsLocalAgentJwt: true, requiresMaterializedRuntimeSkills: false }, + cursor: { supportsInstructionsBundle: true, supportsSkills: true, supportsLocalAgentJwt: true, requiresMaterializedRuntimeSkills: true }, + gemini_local: { supportsInstructionsBundle: true, supportsSkills: true, supportsLocalAgentJwt: true, requiresMaterializedRuntimeSkills: true }, + opencode_local: { supportsInstructionsBundle: true, supportsSkills: true, supportsLocalAgentJwt: true, requiresMaterializedRuntimeSkills: true }, + pi_local: { supportsInstructionsBundle: true, supportsSkills: true, supportsLocalAgentJwt: true, requiresMaterializedRuntimeSkills: true }, + hermes_local: { supportsInstructionsBundle: true, supportsSkills: true, supportsLocalAgentJwt: true, requiresMaterializedRuntimeSkills: false }, + openclaw_gateway: ALL_FALSE, +}; + +/** + * Returns a lookup function that resolves adapter capabilities by type. + * + * Capabilities are fetched from the server adapter listing API and cached + * via react-query. Before the data loads, known built-in adapter types + * return correct synchronous defaults to avoid cold-load regressions. + */ +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(); + if (adapters) { + for (const a of adapters) { + map.set(a.type, a.capabilities); + } + } + return map; + }, [adapters]); + + return (type: string): AdapterCapabilities => + capMap.get(type) ?? KNOWN_DEFAULTS[type] ?? ALL_FALSE; +} diff --git a/ui/src/api/adapters.ts b/ui/src/api/adapters.ts index 86705bd4e7..18b154efed 100644 --- a/ui/src/api/adapters.ts +++ b/ui/src/api/adapters.ts @@ -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) */ diff --git a/ui/src/components/AgentConfigForm.tsx b/ui/src/components/AgentConfigForm.tsx index 065405f9a2..dcf9bf1148 100644 --- a/ui/src/components/AgentConfigForm.tsx +++ b/ui/src/components/AgentConfigForm.tsx @@ -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 }); diff --git a/ui/src/components/OnboardingWizard.tsx b/ui/src/components/OnboardingWizard.tsx index 37509fee19..413c98e21d 100644 --- a/ui/src/components/OnboardingWizard.tsx +++ b/ui/src/components/OnboardingWizard.tsx @@ -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. diff --git a/ui/src/pages/AdapterManager.tsx b/ui/src/pages/AdapterManager.tsx index 0f303bcc2e..acda68778f 100644 --- a/ui/src/pages/AdapterManager.tsx +++ b/ui/src/pages/AdapterManager.tsx @@ -610,6 +610,12 @@ export function AdapterManager() { modelsCount: 0, loaded: true, disabled: virtual.menuDisabled, + capabilities: { + supportsInstructionsBundle: false, + supportsSkills: false, + supportsLocalAgentJwt: false, + requiresMaterializedRuntimeSkills: false, + }, }} canRemove={false} onToggle={(type, disabled) => toggleMutation.mutate({ type, disabled })} diff --git a/ui/src/pages/AgentDetail.tsx b/ui/src/pages/AgentDetail.tsx index 354e432074..2777cb0762 100644 --- a/ui/src/pages/AgentDetail.tsx +++ b/ui/src/pages/AgentDetail.tsx @@ -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),