From acfa5497ef39ab4dd386d3a29ef4acb78c2be0b1 Mon Sep 17 00:00:00 2001 From: Steven Enamakel <31011319+senamakel@users.noreply.github.com> Date: Sat, 4 Apr 2026 02:20:39 -0700 Subject: [PATCH] feat(skills): advanced authentication modes for skills (#315) * feat(auth): implement advanced authentication modes and UI selector - Introduced a new `AuthModeSelector` component for selecting authentication methods (managed, self-hosted, text). - Enhanced the `SkillManager` to handle revocation of both OAuth and auth credentials. - Updated the API to support advanced auth configurations with multiple modes. - Added functionality to persist and restore auth credentials in the skill's data directory. - Updated relevant types and interfaces to accommodate the new auth structure. * feat(auth): enhance SkillSetupWizard with multi-phase authentication flow - Updated `SkillSetupWizard` to support a two-phase authentication process: an optional auth mode selection followed by the setup phase. - Integrated `AuthModeSelector` for users to choose between managed, self-hosted, and text authentication methods. - Improved state management to handle various auth phases, including error handling and transitions to the setup phase upon successful authentication. - Refactored relevant types and interfaces to accommodate the new authentication structure. * refactor(skills): remove unused loader module and clean up bridge imports - Deleted the unused `loader.rs` module, which was reserved for future ES module import support. - Removed references to the `loader` module in `mod.rs`. - Cleaned up the `bridge` module by removing several unused bridge files, including `cron_bridge.rs`, `db.rs`, `log_bridge.rs`, `skills_bridge.rs`, `store.rs`, and `tauri_bridge.rs`. - Added comprehensive tests for the `manifest.rs`, `ops.rs`, and `preferences.rs` modules to ensure functionality remains intact after the refactor. * feat(registry): introduce registry cache management for remote skill registry - Added a new `registry_cache.rs` module to handle disk-based caching for the remote skill registry. - Implemented functions for managing cache, including reading, writing, and checking cache freshness. - Updated `registry_ops.rs` to utilize the new caching functions, improving performance and reducing redundant network calls. - Introduced a new `event_loop` module to manage the QuickJS runtime, including handling incoming messages and persisting state to memory. - Added webhook request handling and RPC message handlers to facilitate communication with external services. - Removed the obsolete `event_loop.rs` file, consolidating functionality into the new structure. * fix(tests): update imports in registry_ops tests to include cache management functions - Modified test module imports in `registry_ops.rs` to include new cache management functions from `registry_cache`. - This change ensures that tests can utilize the updated caching functionality introduced in the recent registry cache management feature. * chore(deps): update OpenHuman version to 0.51.6 and clean up code formatting - Updated the OpenHuman dependency version in `Cargo.lock` to 0.51.6. - Refactored code in several files for improved readability by adjusting formatting and removing unnecessary line breaks. - Ensured consistent logging format in `event_loop` and `rpc_handlers` modules for better clarity in log messages. * refactor(conversations): improve team usage display logic and formatting - Refactored the Conversations component to enhance the display of team usage information, including clearer conditional rendering for budget exhaustion messages. - Improved formatting of budget display for both 5-hour and weekly limits, ensuring consistent presentation. - Streamlined the import of the core HTTP base URL in the WebhooksDebugPanel for better code organization. * refactor(auth): enhance credential management and validation process - Updated the SkillManager to attempt revoking auth credentials even if the auth mode is unknown, improving robustness. - Introduced a new deserialization function to ensure that at least one authentication mode is provided in the SkillAuthConfig. - Enhanced the handling of temporary credential injection during the auth completion process, ensuring credentials are only persisted upon successful validation. - Improved the handling of OAuth credentials in JavaScript, ensuring stale credentials are cleared for non-managed modes. - Added checks for existing Authorization headers to prevent redundant injections in self-hosted modes. * test(manifest): add unit tests for authentication mode deserialization - Introduced tests to validate the deserialization of known authentication modes in the SkillManifest. - Added checks to ensure invalid authentication types and empty mode lists are correctly rejected. - These tests enhance the robustness of the authentication configuration handling in the manifest module. * refactor(messaging): simplify MessagingPanel by removing unused constants and improving input handling - Removed unused constants related to channel statuses, authentication modes, and fallback definitions to streamline the MessagingPanel component. - Updated input handling to use a more concise onChange function, enhancing code readability. - Commented out placeholder and className properties for input fields to improve clarity and focus on essential functionality. * refactor(webhooks): streamline TunnelList imports for improved clarity - Combined the import statements for Tunnel type and tunnelsApi from the same module into a single line, enhancing code readability and organization. --- .../settings/panels/MessagingPanel.tsx | 115 +---- .../settings/panels/WebhooksDebugPanel.tsx | 2 +- .../components/skills/AuthModeSelector.tsx | 139 ++++++ .../components/skills/SetupFormRenderer.tsx | 16 + .../components/skills/SkillSetupWizard.tsx | 409 +++++++++++++++--- app/src/components/webhooks/TunnelList.tsx | 3 +- app/src/lib/skills/manager.ts | 54 ++- app/src/lib/skills/skillsApi.ts | 40 +- app/src/lib/skills/types.ts | 33 +- app/src/pages/Conversations.tsx | 87 ++-- src/openhuman/skills/bridge/cron_bridge.rs | 35 -- src/openhuman/skills/bridge/db.rs | 231 ---------- src/openhuman/skills/bridge/log_bridge.rs | 49 --- src/openhuman/skills/bridge/mod.rs | 15 - src/openhuman/skills/bridge/skills_bridge.rs | 46 -- src/openhuman/skills/bridge/store.rs | 52 --- src/openhuman/skills/bridge/tauri_bridge.rs | 34 -- src/openhuman/skills/loader.rs | 9 - src/openhuman/skills/manifest.rs | 186 ++++++++ src/openhuman/skills/mod.rs | 2 +- src/openhuman/skills/ops.rs | 89 ++++ src/openhuman/skills/preferences.rs | 96 ++++ .../{event_loop.rs => event_loop/mod.rs} | 286 +++--------- .../event_loop/rpc_handlers.rs | 344 +++++++++++++++ .../event_loop/webhook_handler.rs | 97 +++++ .../skills/qjs_skill_instance/instance.rs | 14 +- .../skills/qjs_skill_instance/js_helpers.rs | 64 +++ .../skills/qjs_skill_instance/types.rs | 1 - .../skills/quickjs_libs/bootstrap.js | 123 ++++++ src/openhuman/skills/registry_cache.rs | 80 ++++ src/openhuman/skills/registry_ops.rs | 84 +--- src/openhuman/skills/types.rs | 177 +++++++- 32 files changed, 1996 insertions(+), 1016 deletions(-) create mode 100644 app/src/components/skills/AuthModeSelector.tsx delete mode 100644 src/openhuman/skills/bridge/cron_bridge.rs delete mode 100644 src/openhuman/skills/bridge/db.rs delete mode 100644 src/openhuman/skills/bridge/log_bridge.rs delete mode 100644 src/openhuman/skills/bridge/skills_bridge.rs delete mode 100644 src/openhuman/skills/bridge/store.rs delete mode 100644 src/openhuman/skills/bridge/tauri_bridge.rs delete mode 100644 src/openhuman/skills/loader.rs rename src/openhuman/skills/qjs_skill_instance/{event_loop.rs => event_loop/mod.rs} (73%) create mode 100644 src/openhuman/skills/qjs_skill_instance/event_loop/rpc_handlers.rs create mode 100644 src/openhuman/skills/qjs_skill_instance/event_loop/webhook_handler.rs create mode 100644 src/openhuman/skills/registry_cache.rs diff --git a/app/src/components/settings/panels/MessagingPanel.tsx b/app/src/components/settings/panels/MessagingPanel.tsx index bb8f7dfa0..2f74f1a72 100644 --- a/app/src/components/settings/panels/MessagingPanel.tsx +++ b/app/src/components/settings/panels/MessagingPanel.tsx @@ -19,120 +19,11 @@ import type { ChannelType, } from '../../../types/channels'; import { openUrl } from '../../../utils/openUrl'; -import ChannelCapabilities from '../../channels/ChannelCapabilities'; import ChannelFieldInput from '../../channels/ChannelFieldInput'; import ChannelStatusBadge from '../../channels/ChannelStatusBadge'; import SettingsHeader from '../components/SettingsHeader'; import { useSettingsNavigation } from '../hooks/useSettingsNavigation'; -const STATUS_STYLES: Record = { - connected: { label: 'Connected', className: 'bg-sage-50 text-sage-700 border-sage-200' }, - connecting: { label: 'Connecting', className: 'bg-amber-50 text-amber-700 border-amber-200' }, - disconnected: { - label: 'Disconnected', - className: 'bg-stone-100 text-stone-600 border-stone-200', - }, - error: { label: 'Error', className: 'bg-coral-50 text-coral-600 border-coral-200' }, -}; - -const AUTH_MODE_LABELS: Record = { - managed_dm: 'Managed DM', - oauth: 'OAuth Sign-in', - bot_token: 'Bot Token', - api_key: 'API Key', -}; - -/** Fallback definitions used when the core sidecar is unreachable. */ -const FALLBACK_DEFINITIONS: ChannelDefinition[] = [ - { - id: 'telegram', - display_name: 'Telegram', - description: 'Send and receive messages via Telegram.', - icon: 'telegram', - auth_modes: [ - { - mode: 'bot_token', - description: 'Provide your own Telegram Bot token from @BotFather.', - fields: [ - { - key: 'bot_token', - label: 'Bot Token', - field_type: 'secret', - required: true, - placeholder: '123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11', - }, - { - key: 'allowed_users', - label: 'Allowed Users', - field_type: 'string', - required: false, - placeholder: 'Comma-separated Telegram usernames', - }, - ], - auth_action: undefined, - }, - { - mode: 'managed_dm', - description: 'Message the OpenHuman Telegram bot directly.', - fields: [], - auth_action: 'telegram_managed_dm', - }, - ], - capabilities: ['send_text', 'receive_text', 'typing', 'draft_updates'], - }, - { - id: 'discord', - display_name: 'Discord', - description: 'Send and receive messages via Discord.', - icon: 'discord', - auth_modes: [ - { - mode: 'bot_token', - description: 'Provide your own Discord bot token.', - fields: [ - { - key: 'bot_token', - label: 'Bot Token', - field_type: 'secret', - required: true, - placeholder: 'Your Discord bot token', - }, - { - key: 'guild_id', - label: 'Server (Guild) ID', - field_type: 'string', - required: false, - placeholder: 'Optional: restrict to a specific server', - }, - ], - auth_action: undefined, - }, - { - mode: 'oauth', - description: 'Install the OpenHuman bot to your Discord server via OAuth.', - fields: [], - auth_action: 'discord_oauth', - }, - ], - capabilities: ['send_text', 'receive_text', 'typing', 'threaded_replies'], - }, - { - id: 'web', - display_name: 'Web', - description: 'Chat via the built-in web UI.', - icon: 'web', - auth_modes: [ - { - mode: 'managed_dm', - description: 'Use the embedded web chat — no setup required.', - fields: [], - auth_action: undefined, - }, - ], - capabilities: ['send_text', 'send_rich_text', 'receive_text'], - }, -]; - const MessagingPanel = () => { const { navigateBack } = useSettingsNavigation(); const dispatch = useAppDispatch(); @@ -364,9 +255,9 @@ const MessagingPanel = () => { key={field.key} field={field} value={fieldValues[compositeKey]?.[field.key] ?? ''} - onChange={e => updateField(compositeKey, field.key, e.target.value)} - placeholder={field.placeholder || field.label} - className="w-full rounded-lg border border-stone-200 bg-white px-3 py-2 text-sm text-stone-900 placeholder:text-stone-400 focus:outline-none focus:border-primary-500/60" + onChange={value => updateField(compositeKey, field.key, value)} + // placeholder={field.placeholder || field.label} + // className="w-full rounded-lg border border-stone-200 bg-white px-3 py-2 text-sm text-stone-900 placeholder:text-stone-400 focus:outline-none focus:border-primary-500/60" /> ))} diff --git a/app/src/components/settings/panels/WebhooksDebugPanel.tsx b/app/src/components/settings/panels/WebhooksDebugPanel.tsx index 925cb1703..88d32afe8 100644 --- a/app/src/components/settings/panels/WebhooksDebugPanel.tsx +++ b/app/src/components/settings/panels/WebhooksDebugPanel.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; -import { getCoreHttpBaseUrl } from '../../../services/coreRpcClient'; import { tunnelsApi } from '../../../services/api/tunnelsApi'; +import { getCoreHttpBaseUrl } from '../../../services/coreRpcClient'; import { BACKEND_URL } from '../../../utils/config'; import { openhumanWebhooksClearLogs, diff --git a/app/src/components/skills/AuthModeSelector.tsx b/app/src/components/skills/AuthModeSelector.tsx new file mode 100644 index 000000000..a4e6ebdf3 --- /dev/null +++ b/app/src/components/skills/AuthModeSelector.tsx @@ -0,0 +1,139 @@ +/** + * Card-based selector for skill authentication modes. + * Displays available auth modes (managed, self_hosted, text) as clickable cards. + * For managed mode, clicking immediately triggers the OAuth flow. + */ + +import type { ReactElement } from "react"; +import type { AuthMode } from "../../lib/skills/types.ts"; + +interface AuthModeSelectorProps { + modes: AuthMode[]; + onSelect: (mode: AuthMode) => void; + disabled?: boolean; +} + +const MODE_ICONS: Record ReactElement> = { + managed: ({ className }) => ( + + + + ), + self_hosted: ({ className }) => ( + + + + ), + text: ({ className }) => ( + + + + ), +}; + +const DEFAULT_LABELS: Record = { + managed: "OpenHuman Managed", + self_hosted: "Self-hosted", + text: "Credential Text", +}; + +const DEFAULT_DESCRIPTIONS: Record = { + managed: "One-click setup through OpenHuman", + self_hosted: "Enter your own API credentials", + text: "Paste credential content directly", +}; + +function formatProviderName(provider: string): string { + const names: Record = { + notion: "Notion", + google: "Google", + github: "GitHub", + slack: "Slack", + discord: "Discord", + twitter: "Twitter", + linear: "Linear", + gitlab: "GitLab", + }; + return names[provider] ?? provider.charAt(0).toUpperCase() + provider.slice(1); +} + +export default function AuthModeSelector({ + modes, + onSelect, + disabled, +}: AuthModeSelectorProps) { + return ( +
+
+

+ Choose how to connect +

+

+ Select an authentication method +

+
+ + {modes.map((mode) => { + const IconComponent = MODE_ICONS[mode.type] ?? MODE_ICONS.self_hosted; + const label = + mode.type === "managed" && mode.provider + ? mode.label ?? `Connect with ${formatProviderName(mode.provider)}` + : mode.label ?? DEFAULT_LABELS[mode.type] ?? mode.type; + const description = + mode.description ?? DEFAULT_DESCRIPTIONS[mode.type] ?? ""; + + return ( + + ); + })} +
+ ); +} diff --git a/app/src/components/skills/SetupFormRenderer.tsx b/app/src/components/skills/SetupFormRenderer.tsx index dd25d45d0..f9624fe38 100644 --- a/app/src/components/skills/SetupFormRenderer.tsx +++ b/app/src/components/skills/SetupFormRenderer.tsx @@ -174,6 +174,22 @@ export default function SetupFormRenderer({ )} + {/* Textarea */} + {field.type === "textarea" && ( +