Files
fusion/packages/dashboard/src/runtime-provider-probes.ts
gsxdsm a3102080c4 feat(paperclip-runtime): route Paperclip calls through paperclipai CLI in Local CLI mode
The Paperclip runtime card's Local CLI tab previously only derived an apiUrl
from the local config and then made HTTP calls itself, so the Test action and
the company/agent pickers ignored the user's onboarded CLI auth context.

Add CLI-backed variants for every Paperclip call that has a `paperclipai`
counterpart, and route through them when transport=cli — both from the
settings card and from the runtime adapter's prompt path.

- New plugin functions spawning `paperclipai … --json`:
  * probePaperclipViaCli, listCompaniesViaCli, listCompanyAgentsViaCli
    (settings card test + pickers)
  * createIssueViaCli, getIssueViaCli, agentsMeViaCli (runtime hot path)
- New dashboard routes: /providers/paperclip/cli-status, /cli-companies,
  /cli-agents (read-only façades over the plugin's CLI helpers)
- PaperclipRuntimeCard: branches on transport=cli to use the cli-* fetchers
- PaperclipRuntimeAdapter: stores transport on the session and routes
  createIssue/getIssue + identity derivation through CLI variants in CLI mode;
  raises a clear error when agentId is unset in CLI mode (paperclipai has no
  /agents/me equivalent)
- getIssueComments / wakeAgent / getRunEvents stay on HTTP (no matching
  paperclipai subcommands) and continue to use the apiKey discovered from
  the local paperclipai config, so CLI mode still works end-to-end
- Tests: 9 new paperclip-client tests covering each CLI variant + 5 new
  adapter tests for the Local-CLI transport branch (64/64 plugin tests pass)
- Update routes.test for the existing BUNDLED_PLUGIN_RUNTIMES fallback so
  the bundled hermes/openclaw/paperclip entries are expected alongside
  installed plugins

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:06:20 -07:00

207 lines
6.0 KiB
TypeScript

/**
* Thin façade over the three runtime-provider plugin probe functions.
*
* Each exported function delegates directly to the corresponding plugin's
* probe. The indirection exists so that:
* 1. Route handlers import from one stable internal module rather than
* reaching into plugin packages directly.
* 2. Tests can spy/mock at this module boundary without touching the
* plugin packages.
* 3. If a plugin package is somehow not installed, the error surfaces
* as an import-time TypeError with a clear message rather than a
* cryptic missing-module crash inside a route handler.
*/
import {
listHermesProfiles,
probeHermesBinary,
type HermesBinaryStatus,
type HermesProfileSummary,
} from "@fusion-plugin-examples/hermes-runtime";
import {
probeOpenClawBinary,
type OpenClawBinaryStatus,
} from "@fusion-plugin-examples/openclaw-runtime";
import {
agentsMe,
discoverPaperclipCliConfig,
listCompanies,
listCompaniesViaCli,
listCompanyAgents,
listCompanyAgentsViaCli,
mintAgentApiKeyViaCli,
probePaperclipConnection,
probePaperclipViaCli,
type MintCliKeyOptions,
type MintedApiKey,
type PaperclipAgentSummary,
type PaperclipCliDiscoveryResult,
type PaperclipCompanySummary,
type PaperclipConnectionStatus,
} from "@fusion-plugin-examples/paperclip-runtime";
export type {
HermesBinaryStatus,
HermesProfileSummary,
MintCliKeyOptions,
MintedApiKey,
OpenClawBinaryStatus,
PaperclipAgentSummary,
PaperclipCliDiscoveryResult,
PaperclipCompanySummary,
PaperclipConnectionStatus,
};
export { mintAgentApiKeyViaCli };
/**
* Probe the local Hermes binary.
*
* Never throws — failures are reported as `available: false` with a reason
* field so HTTP handlers can render the provider card without try/catch.
*/
export async function probeHermesProvider(opts?: {
binaryPath?: string;
}): Promise<HermesBinaryStatus> {
return probeHermesBinary(opts);
}
/**
* List Hermes profiles via `hermes profile list`.
*
* Delegates directly to the plugin's listHermesProfiles.
* Callers are expected to handle errors; this function does not swallow them.
*/
export async function listHermesProviderProfiles(opts?: {
binaryPath?: string;
}): Promise<HermesProfileSummary[]> {
return listHermesProfiles(opts);
}
/**
* Probe the local OpenClaw binary.
*
* Never throws — failures are reported as `available: false` with a reason.
*/
export async function probeOpenClawProvider(opts?: {
binaryPath?: string;
}): Promise<OpenClawBinaryStatus> {
return probeOpenClawBinary(opts);
}
/**
* Probe a Paperclip server by its API URL.
*
* Never throws — failures are reported as `available: false` with a reason.
*/
export async function probePaperclipProvider(opts: {
apiUrl: string;
apiKey?: string;
}): Promise<PaperclipConnectionStatus> {
return probePaperclipConnection(opts);
}
/**
* List companies visible to the bearer. Falls back to the single company
* derived from `/api/agents/me` when `/api/companies` returns 403 (typical
* for agent-key-scoped requests in `authenticated` deployment mode).
*
* Always returns at least an empty array; never throws — failures degrade
* to an empty list so the UI can render a "no companies discovered" state.
*/
export async function listPaperclipCompanies(opts: {
apiUrl: string;
apiKey?: string;
}): Promise<PaperclipCompanySummary[]> {
// Prefer the broad listing if it works (board cookie / multi-company access).
try {
const cs = await listCompanies(opts.apiUrl, opts.apiKey);
if (cs.length > 0) return cs;
} catch {
// Fall through to /agents/me below.
}
// Fall back: agent keys can see their own company via /agents/me.
try {
const me = await agentsMe(opts.apiUrl, opts.apiKey);
return [{ id: me.companyId, name: me.companyName ?? me.companyId }];
} catch {
return [];
}
}
export async function listPaperclipCompanyAgents(opts: {
apiUrl: string;
apiKey?: string;
companyId: string;
}): Promise<PaperclipAgentSummary[]> {
return listCompanyAgents(opts.apiUrl, opts.apiKey, opts.companyId);
}
export async function discoverPaperclipCli(opts: {
cliConfigPath?: string;
}): Promise<PaperclipCliDiscoveryResult> {
return discoverPaperclipCliConfig({ configPath: opts.cliConfigPath });
}
/**
* Probe Paperclip through the local `paperclipai` CLI. Used by the dashboard's
* "Local CLI" tab so the test action exercises the same code path as actual
* CLI-mode runtime calls (carries the user's onboarded CLI context).
*/
export async function probePaperclipViaCliFacade(opts: {
cliBinaryPath?: string;
cliConfigPath?: string;
}): Promise<PaperclipConnectionStatus> {
return probePaperclipViaCli({
cliBinaryPath: opts.cliBinaryPath,
cliConfigPath: opts.cliConfigPath,
});
}
export async function listPaperclipCompaniesViaCliFacade(opts: {
cliBinaryPath?: string;
cliConfigPath?: string;
}): Promise<PaperclipCompanySummary[]> {
try {
return await listCompaniesViaCli({
cliBinaryPath: opts.cliBinaryPath,
cliConfigPath: opts.cliConfigPath,
});
} catch {
return [];
}
}
export async function listPaperclipCompanyAgentsViaCliFacade(opts: {
cliBinaryPath?: string;
cliConfigPath?: string;
companyId: string;
}): Promise<PaperclipAgentSummary[]> {
try {
return await listCompanyAgentsViaCli({
cliBinaryPath: opts.cliBinaryPath,
cliConfigPath: opts.cliConfigPath,
companyId: opts.companyId,
});
} catch {
return [];
}
}
/**
* Thin façade over `mintAgentApiKeyViaCli` that never throws.
* Returns `{ ok: true, key }` on success or `{ ok: false, reason }` on failure,
* so HTTP handlers and tests can destructure without try/catch.
*/
export async function mintPaperclipKeyViaCli(
opts: MintCliKeyOptions,
): Promise<{ ok: true; key: MintedApiKey } | { ok: false; reason: string }> {
try {
const key = await mintAgentApiKeyViaCli(opts);
return { ok: true, key };
} catch (err) {
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
}
}