From d4efc089096bac475f84c53ec8d17d97eb5956c1 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sun, 10 May 2026 16:35:40 +0100 Subject: [PATCH] =?UTF-8?q?Surface=20agent=20integrations=20in=20Settings?= =?UTF-8?q?=20=E2=86=92=20API=20Access?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The agent substrate has been entirely API-side until now. An operator opening Pulse had no way to know it existed, no way to see what an agent connected to their instance could do, and no quick path to a working MCP integration. Slice 59 closes that visibility gap. A new AgentIntegrationsPanel renders below the existing APITokenManager on the API Access tab. The panel: - Fetches /api/agent/capabilities at mount and renders the declared capabilities grouped by category (Context, Operator state, Patrol findings, Action governance), each row showing name, method+path, scope chip, description, and the stable error codes the manifest declares. Adding a capability on the backend extends this list automatically; nothing in the panel is hardcoded against the substrate's surface. - Generates an MCP config snippet using window.location.origin so the snippet is correct for whichever URL the operator is reading from. Includes a copy-to-clipboard button (matching the existing CopyCommandBlock pattern) and a brief explainer pointing at the right config path for Claude Desktop and Claude Code. - Links to cmd/pulse-mcp/README.md, cmd/agent-probe, and docs/AGENT_SUBSTRATE.md so an integrator has the full setup story without leaving the panel. Wiring is one import + one component placement in APIAccessPanel.tsx. The api tab already concerns "what can be done with API tokens"; adding the agent surface as a sibling section under the same tab keeps the operator's mental model coherent (one place for machine-driven access) and avoids growing the Settings tab inventory or touching settingsNavigationModel, the registry, the loaders, or the routing tests. Two architectural pins land alongside: - settingsArchitecture.test.ts gains a guardrail that the AgentIntegrationsPanel sits as a sibling section inside APIAccessPanel rather than being lifted into its own tab. Drift would fragment the agent surface across navigation. - The frontend-primitives contract documents the sibling-panel-over-new-tab pattern for additive operator surfaces closely related to an existing tab's intent. - The security-privacy contract documents the new section's presence on the API Access tab and pins that token minting still flows through APITokenManager — the new section surfaces what tokens unlock, not a parallel auth path. Verified against the running dev server: the page renders the four category sections (Context, Operator state, Patrol findings, Action governance), the live MCP config snippet contains the deployment's own origin, and the manifest endpoint serves all 14 capabilities (the 11 substrate capabilities + the 3 action capabilities from slice 58). TypeScript clean across the frontend. --- .../subsystems/frontend-primitives.md | 13 + .../internal/subsystems/security-privacy.md | 16 ++ .../components/Settings/APIAccessPanel.tsx | 3 + .../Settings/AgentIntegrationsPanel.tsx | 255 ++++++++++++++++++ .../__tests__/settingsArchitecture.test.ts | 13 + 5 files changed, 300 insertions(+) create mode 100644 frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsx diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 64bac65e2..699b48d82 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -313,6 +313,19 @@ prompt explain the same operator-facing priority. shortcut chip. 2. Route new top-level settings surfaces through the canonical settings shell instead of introducing page-local framing. + When a new operator-facing concern is closely related to an + existing tab's intent, prefer adding it as a sibling + `SettingsPanel` inside that tab's container component over + minting a new top-level tab. The agent-integrations surface + added in slice 59 follows this pattern: + `frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsx` + ships under the existing API Access tab via + `APIAccessPanel.tsx`'s composition, not as its own tab. This + keeps the tab inventory bounded, avoids touching + `settingsNavigationModel.ts`, the registry, the loaders, and + the routing tests for additive sub-surfaces, and presents + "tokens + what those tokens unlock" as one operator-facing + story. Shared shells and primitives that need websocket or dark-mode context must consume `frontend-modern/src/contexts/appRuntime.ts`; they must not import `frontend-modern/src/App.tsx`, because `App.tsx` owns provider placement diff --git a/docs/release-control/v6/internal/subsystems/security-privacy.md b/docs/release-control/v6/internal/subsystems/security-privacy.md index adc92246f..f5c52f835 100644 --- a/docs/release-control/v6/internal/subsystems/security-privacy.md +++ b/docs/release-control/v6/internal/subsystems/security-privacy.md @@ -278,6 +278,22 @@ helper. Security surfaces may consume compatibility-normalized `platformType: 'truenas'` resources, but they must not reintroduce a separate `resource.type === 'truenas'` trust path when calculating token usage, revocation targets, or operator-facing token ownership. +The API Access tab now hosts an Agent Integrations section +(`frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsx`) +alongside the existing API Token Manager. The section reads +`/api/agent/capabilities` at mount and renders the declared +agent surface (capabilities grouped by category, stable error +codes, scopes) plus an MCP config snippet generated from the +deployment's own origin so an operator wiring Claude Desktop or +Claude Code sees the right base URL automatically. The section +does NOT introduce a new token-mint flow or auth path: tokens +still flow through the API Token Manager, and the snippet +documents the `monitoring:read` / `monitoring:write` scopes the +agent surface requires. Keeping token minting and agent-surface +disclosure on the same tab means an operator gets a single +"machine-driven access" mental model rather than two surfaces +to reconcile. + That same token-management boundary also reserves token-owner identity for the server-authenticated principal. Token-minting helpers must derive `owner_user_id` from the authenticated session or caller token and reject any diff --git a/frontend-modern/src/components/Settings/APIAccessPanel.tsx b/frontend-modern/src/components/Settings/APIAccessPanel.tsx index 30e7102f5..69dca44e8 100644 --- a/frontend-modern/src/components/Settings/APIAccessPanel.tsx +++ b/frontend-modern/src/components/Settings/APIAccessPanel.tsx @@ -3,6 +3,7 @@ import SettingsPanel from '@/components/shared/SettingsPanel'; import { API_TOKEN_ACCESS_PANEL_DESCRIPTION } from '@/utils/apiTokenPresentation'; import { API_TOKEN_SCOPES_DOC_URL } from '@/utils/docsLinks'; import APITokenManager from './APITokenManager'; +import AgentIntegrationsPanel from './AgentIntegrationsPanel'; interface APIAccessPanelProps { currentTokenHint?: string; @@ -37,6 +38,8 @@ export const APIAccessPanel: Component = (props) => { refreshing={props.refreshing} canManage={props.canManage} /> + + ); }; diff --git a/frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsx b/frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsx new file mode 100644 index 000000000..3579fe39b --- /dev/null +++ b/frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsx @@ -0,0 +1,255 @@ +import { Component, createMemo, createResource, createSignal, For, Show } from 'solid-js'; +import SettingsPanel from '@/components/shared/SettingsPanel'; +import CopyCommandBlock from './CopyCommandBlock'; + +// AgentCapability mirrors the wire shape of one entry in +// /api/agent/capabilities. Defined inline so this component does +// not depend on a shared API client; the manifest endpoint is +// public and stable. +interface AgentCapability { + name: string; + description: string; + category: string; + method: string; + path: string; + scope: string; + responseShape?: string; + errorCodes?: string[]; + requestBodyShape?: string; +} + +interface AgentCapabilitiesManifest { + version: string; + capabilities: AgentCapability[]; +} + +// Category presentation order on screen. Matches the closed set +// the backend pin enforces (see TestAgentCapabilitiesManifest_ +// CategoriesAreClosed). Capabilities with an unknown category are +// rendered last under a generic "other" heading so a future +// addition that the panel does not yet know about still renders. +const CATEGORY_ORDER: ReadonlyArray<{ id: string; label: string; description: string }> = [ + { + id: 'context', + label: 'Context', + description: 'Discovery and read-only situated reads. Agents start here.', + }, + { + id: 'operator-state', + label: 'Operator state', + description: 'Per-resource intent: intentionally-offline, never-auto-remediate, maintenance windows.', + }, + { + id: 'finding', + label: 'Patrol findings', + description: 'Acknowledge, snooze, dismiss, or resolve findings the patrol runtime raised.', + }, + { + id: 'action', + label: 'Action governance', + description: 'Plan, approve, and execute capability invocations against a resource.', + }, +]; + +async function fetchManifest(): Promise { + const response = await fetch('/api/agent/capabilities', { + headers: { Accept: 'application/json' }, + }); + if (!response.ok) { + throw new Error(`manifest fetch failed: ${response.status}`); + } + return (await response.json()) as AgentCapabilitiesManifest; +} + +// formatMcpConfig builds the JSON snippet an integrator drops into +// claude_desktop_config.json (or .mcp.json for Claude Code). The +// host is the deployment's own origin so the snippet is correct +// for whatever URL the operator is reading the panel from. +function formatMcpConfig(origin: string): string { + const config = { + mcpServers: { + pulse: { + command: 'pulse-mcp', + args: ['--base-url', origin], + env: { + PULSE_API_TOKEN: '', + }, + }, + }, + }; + return JSON.stringify(config, null, 2); +} + +const AGENT_PROBE_README = 'https://github.com/rcourtman/Pulse/blob/main/cmd/agent-probe/main.go'; +const PULSE_MCP_README = 'https://github.com/rcourtman/Pulse/blob/main/cmd/pulse-mcp/README.md'; +const SUBSTRATE_DOC = 'https://github.com/rcourtman/Pulse/blob/main/docs/AGENT_SUBSTRATE.md'; + +export const AgentIntegrationsPanel: Component = () => { + const [manifest] = createResource(fetchManifest); + const [copied, setCopied] = createSignal(null); + const origin = typeof window !== 'undefined' ? window.location.origin : 'http://localhost:7655'; + const mcpConfig = formatMcpConfig(origin); + + const grouped = createMemo(() => { + const m = manifest(); + if (!m) return []; + // Index capabilities by category, then return them in + // CATEGORY_ORDER. Unknown categories spill into a generic + // "other" bucket so the panel never silently drops a + // capability the operator should be able to see. + const byCategory = new Map(); + for (const cap of m.capabilities) { + const list = byCategory.get(cap.category) ?? []; + list.push(cap); + byCategory.set(cap.category, list); + } + const sections: Array<{ id: string; label: string; description?: string; entries: AgentCapability[] }> = []; + for (const known of CATEGORY_ORDER) { + const entries = byCategory.get(known.id); + if (entries && entries.length > 0) { + sections.push({ id: known.id, label: known.label, description: known.description, entries }); + byCategory.delete(known.id); + } + } + for (const [unknownCategory, entries] of byCategory) { + sections.push({ id: unknownCategory, label: unknownCategory, entries }); + } + return sections; + }); + + const handleCopySnippet = async (snippet: string) => { + await navigator.clipboard.writeText(snippet); + setCopied(snippet); + window.setTimeout(() => setCopied(null), 2000); + }; + + return ( + +
+
+

+ Pulse exposes a manifest-driven agent surface for MCP and HTTP clients. An external + agent (Claude Desktop, Claude Code, or a custom integration) discovers what it can do + via /api/agent/capabilities, then calls the + declared endpoints with an API token. +

+

+ The capabilities below are read live from this Pulse instance. Adding a capability on + the backend adds it here automatically; nothing in this panel is hardcoded. +

+
+ +
+
+

Claude Desktop / Claude Code config

+ + Copied to clipboard + +
+

+ Drop this block into{' '} + ~/Library/Application Support/Claude/claude_desktop_config.json{' '} + (Claude Desktop) or your project's .mcp.json (Claude + Code). Mint a token below with monitoring:read (and{' '} + monitoring:write for the operator-state write tools), then + replace <your-api-token>. +

+ +

+ See{' '} + + cmd/pulse-mcp/README.md + {' '} + for build instructions, the --emit-notifications flag, + and known limitations. The companion HTTP example is at{' '} + + cmd/agent-probe + + . Background and the substrate's design notes live in{' '} + + docs/AGENT_SUBSTRATE.md + + . +

+
+ + +

Loading capabilities…

+
+ + +

+ Could not load /api/agent/capabilities:{' '} + {String(manifest.error)} +

+
+ + 0}> +
+
+

+ Declared capabilities (manifest {manifest()?.version}) +

+ + {manifest()?.capabilities.length ?? 0} total + +
+ + {(section) => ( +
+
+

+ {section.label} +

+ +

{section.description}

+
+
+
    + + {(cap) => ( +
  • +
    + + {cap.name} + + + {cap.method} {cap.path} + + + {cap.scope} + +
    +

    {cap.description}

    + 0}> +

    + Stable error codes:{' '} + + {(code, idx) => ( + <> + {code} + {idx() < (cap.errorCodes!.length - 1) ? ', ' : ''} + + )} + +

    +
    +
  • + )} +
    +
+
+ )} +
+
+
+
+
+ ); +}; + +export default AgentIntegrationsPanel; diff --git a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts index 8f1367a43..3e2f450c6 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts @@ -635,6 +635,19 @@ describe('settings architecture guardrails', () => { ); }); + it('exposes the agent integrations panel under API Access without growing the settings tab inventory', () => { + // The agent integrations panel sits as a sibling section + // under the existing API Access tab rather than as its own + // navigation entry. This pin keeps the operator's mental + // model coherent (one tab for machine-driven access: + // tokens + what those tokens unlock) and prevents drift + // toward fragmenting the agent surface across tabs. + expect(apiAccessPanelSource).toContain( + "import AgentIntegrationsPanel from './AgentIntegrationsPanel';", + ); + expect(apiAccessPanelSource).toContain(''); + }); + it('keeps internal analytics off the user diagnostics boundary', () => { expect(diagnosticsResultsPanelSource).not.toContain('Commercial Funnel'); expect(diagnosticsResultsPanelSource).not.toContain('Infrastructure Onboarding');