fix(channels): correct channel-setup nav paths, add in-app guidance (#4910)

Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
This commit is contained in:
YellowSnnowmann
2026-07-16 03:05:20 +03:00
committed by GitHub
co-authored by Steven Enamakel
parent aa54d0dcda
commit fae9ba011f
43 changed files with 315 additions and 84 deletions
@@ -0,0 +1,23 @@
import { describe, expect, it } from 'vitest';
import { renderWithProviders } from '../../test/test-utils';
import ChannelConnectHelp from './ChannelConnectHelp';
describe('<ChannelConnectHelp /> (issue #4884)', () => {
it('renders grounded connect steps for Discord', () => {
const { getByText } = renderWithProviders(<ChannelConnectHelp channelId="discord" />);
expect(getByText('How to connect')).toBeInTheDocument();
expect(getByText(/Discord developer portal/i)).toBeInTheDocument();
});
it('renders grounded connect steps for Telegram', () => {
const { getByText } = renderWithProviders(<ChannelConnectHelp channelId="telegram" />);
expect(getByText('How to connect')).toBeInTheDocument();
expect(getByText(/@BotFather/i)).toBeInTheDocument();
});
it('renders nothing for a channel without documented guidance', () => {
const { container } = renderWithProviders(<ChannelConnectHelp channelId="lark" />);
expect(container).toBeEmptyDOMElement();
});
});
@@ -0,0 +1,32 @@
/**
* In-app "how to connect" guidance shown at the top of a channel's setup card.
*
* Grounds users in the real connect flow so they don't have to ask the agent
* for a navigation path (the agent previously hallucinated non-existent menus
* like "Settings → Automation & Channels"). Only channels with a documented
* flow render a callout; everything else renders nothing.
*/
import { useT } from '../../lib/i18n/I18nContext';
/** Per-channel help copy. Add a key here to surface guidance for a channel. */
const CHANNEL_HELP_KEY: Record<string, string> = {
discord: 'channels.connectHelp.discord',
telegram: 'channels.connectHelp.telegram',
};
interface ChannelConnectHelpProps {
channelId: string;
}
export default function ChannelConnectHelp({ channelId }: ChannelConnectHelpProps) {
const { t } = useT();
const bodyKey = CHANNEL_HELP_KEY[channelId];
if (!bodyKey) return null;
return (
<div className="rounded-lg border border-primary-200 dark:border-primary-500/30 bg-primary-50/80 dark:bg-primary-500/10 px-4 py-3 text-sm text-content-secondary">
<p className="font-medium text-content">{t('channels.connectHelp.title')}</p>
<p className="mt-1 text-xs text-content-secondary">{t(bodyKey)}</p>
</div>
);
}
@@ -10,6 +10,7 @@ import { useT } from '../../lib/i18n/I18nContext';
import type { ChannelDefinition, ChannelType } from '../../types/channels';
import { CloseIcon } from '../ui';
import Button from '../ui/Button';
import ChannelConnectHelp from './ChannelConnectHelp';
import { renderChannelIcon } from './channelIcon';
import CredentialChannelConfig from './CredentialChannelConfig';
import DiscordConfig from './DiscordConfig';
@@ -21,9 +22,11 @@ interface ChannelSetupModalProps {
onClose: () => void;
}
function ChannelConfigContent({ definition }: { definition: ChannelDefinition }) {
const { t } = useT();
const channelId = definition.id as ChannelType;
function renderChannelConfig(
definition: ChannelDefinition,
channelId: ChannelType,
t: (key: string, fallback?: string) => string
) {
switch (channelId) {
case 'telegram':
return <TelegramConfig definition={definition} />;
@@ -47,6 +50,17 @@ function ChannelConfigContent({ definition }: { definition: ChannelDefinition })
}
}
function ChannelConfigContent({ definition }: { definition: ChannelDefinition }) {
const { t } = useT();
const channelId = definition.id as ChannelType;
return (
<div className="space-y-3">
<ChannelConnectHelp channelId={channelId} />
{renderChannelConfig(definition, channelId, t)}
</div>
);
}
export default function ChannelSetupModal({ definition, onClose }: ChannelSetupModalProps) {
const { t } = useT();
const modalRef = useRef<HTMLDivElement>(null);
+7
View File
@@ -3386,6 +3386,13 @@ const messages: TranslationMap = {
'channels.telegram.remoteControlTitle': 'جهاز التحكم عن بعد (Telegram)',
'channels.telegram.remoteControlBody':
'من دردشة Telegram المسموح بها، أرسل /الحالة، /الجلسات، /جديد، أو /مساعدة. لا يزال توجيه النموذج يستخدم /model و /models.',
'channels.connectHelp.title': 'كيفية الاتصال',
'channels.connectHelp.discord':
'اختر طريقة أدناه: اربط حسابك عبر OpenHuman، أو ثبّت البوت باستخدام OAuth، أو الصق رمز البوت الخاص بك من بوابة مطوري Discord.',
'channels.connectHelp.telegram':
'اختر طريقة أدناه: راسل بوت OpenHuman المُدار لربطه، أو الصق رمز البوت الخاص بك من @BotFather.',
'channels.connectHelp.slackNote':
'تبحث عن Slack؟ يتصل Slack كتطبيق من خلال الاتصالات → OAuth، وليس كقناة مراسلة هنا.',
'channels.web.displayName': 'الويب',
'channels.web.description': 'الدردشة عبر واجهة مستخدم الويب المضمنة.',
'channels.web.authMode.managed_dm.description': 'استخدم دردشة الويب المضمنة - لا يلزم الإعداد.',
+7
View File
@@ -3464,6 +3464,13 @@ const messages: TranslationMap = {
'channels.telegram.remoteControlTitle': 'রিমোট কন্ট্রোল (Telegram)',
'channels.telegram.remoteControlBody':
'একটি অনুমোদিত Telegram চ্যাট থেকে, /status, /sessions, /new, অথবা /help পাঠান। মডেল রাউটিং এখনও /মডেল এবং /মডেল ব্যবহার করে।',
'channels.connectHelp.title': 'কীভাবে সংযুক্ত করবেন',
'channels.connectHelp.discord':
'নিচে একটি পদ্ধতি বেছে নিন: OpenHuman-এর মাধ্যমে আপনার অ্যাকাউন্ট লিঙ্ক করুন, OAuth দিয়ে বট ইনস্টল করুন, অথবা Discord ডেভেলপার পোর্টাল থেকে আপনার নিজের বট টোকেন পেস্ট করুন।',
'channels.connectHelp.telegram':
'নিচে একটি পদ্ধতি বেছে নিন: লিঙ্ক করতে ম্যানেজড OpenHuman বটে বার্তা পাঠান, অথবা @BotFather থেকে আপনার নিজের বট টোকেন পেস্ট করুন।',
'channels.connectHelp.slackNote':
'Slack খুঁজছেন? Slack এখানে মেসেজিং চ্যানেল হিসেবে নয়, সংযোগ → OAuth-এ একটি অ্যাপ হিসেবে সংযুক্ত হয়।',
'channels.web.displayName': 'ওয়েব',
'channels.web.description': 'বিল্ট-ইন ওয়েব UI এর মাধ্যমে চ্যাট করুন।',
'channels.web.authMode.managed_dm.description':
+7
View File
@@ -3564,6 +3564,13 @@ const messages: TranslationMap = {
'channels.telegram.remoteControlTitle': 'Fernsteuerung (Telegram)',
'channels.telegram.remoteControlBody':
'Senden Sie von einem zulässigen Telegram-Chat aus /status, /sessions, /new oder /help. Das Modellrouting verwendet weiterhin /model und /models.',
'channels.connectHelp.title': 'So verbindest du',
'channels.connectHelp.discord':
'Wähle unten eine Methode: verknüpfe dein Konto über OpenHuman, installiere den Bot per OAuth oder füge deinen eigenen Bot-Token aus dem Discord-Entwicklerportal ein.',
'channels.connectHelp.telegram':
'Wähle unten eine Methode: schreibe dem verwalteten OpenHuman-Bot, um ihn zu verknüpfen, oder füge deinen eigenen Bot-Token von @BotFather ein.',
'channels.connectHelp.slackNote':
'Du suchst Slack? Slack wird als App unter Verbindungen → OAuth verbunden, nicht als Messaging-Kanal hier.',
'channels.web.displayName': 'Web',
'channels.web.description': 'Chatte über die integrierte Web-Oberfläche.',
'channels.web.authMode.managed_dm.description':
+9
View File
@@ -3879,6 +3879,15 @@ const en: TranslationMap = {
'channels.telegram.remoteControlBody':
'From an allowed Telegram chat, send /status, /sessions, /new, or /help. Model routing still uses /model and /models.',
// Connect help (in-app guidance so users do not have to ask the agent for the path)
'channels.connectHelp.title': 'How to connect',
'channels.connectHelp.discord':
'Pick a method below: link your account via OpenHuman, install the bot with OAuth, or paste your own bot token from the Discord developer portal.',
'channels.connectHelp.telegram':
'Pick a method below: message the managed OpenHuman bot to link it, or paste your own bot token from @BotFather.',
'channels.connectHelp.slackNote':
'Looking for Slack? Slack connects as an app under Connections → OAuth, not as a messaging channel here.',
// Web
'channels.web.displayName': 'Web',
'channels.web.description': 'Chat via the built-in web UI.',
+7
View File
@@ -3528,6 +3528,13 @@ const messages: TranslationMap = {
'channels.telegram.remoteControlTitle': 'Control remoto (Telegram)',
'channels.telegram.remoteControlBody':
'Desde un chat Telegram permitido, envíe /status, /sessions, /new o /help. El enrutamiento de modelos todavía usa /model y /models.',
'channels.connectHelp.title': 'Cómo conectar',
'channels.connectHelp.discord':
'Elige un método abajo: vincula tu cuenta con OpenHuman, instala el bot con OAuth o pega tu propio token de bot del portal para desarrolladores de Discord.',
'channels.connectHelp.telegram':
'Elige un método abajo: escribe al bot gestionado de OpenHuman para vincularlo, o pega tu propio token de bot de @BotFather.',
'channels.connectHelp.slackNote':
'¿Buscas Slack? Slack se conecta como una app en Conexiones → OAuth, no como un canal de mensajería aquí.',
'channels.web.displayName': 'Web',
'channels.web.description': 'Chatea a través de la interfaz de usuario web incorporada.',
'channels.web.authMode.managed_dm.description':
+7
View File
@@ -3554,6 +3554,13 @@ const messages: TranslationMap = {
'channels.telegram.remoteControlTitle': 'Télécommande (Telegram)',
'channels.telegram.remoteControlBody':
"À partir d'un chat Telegram autorisé, envoyez /status, /sessions, /new ou /help. Le routage de modèles utilise toujours /model et /models.",
'channels.connectHelp.title': 'Comment se connecter',
'channels.connectHelp.discord':
'Choisissez une méthode ci-dessous : reliez votre compte via OpenHuman, installez le bot avec OAuth, ou collez votre propre jeton de bot depuis le portail développeur Discord.',
'channels.connectHelp.telegram':
'Choisissez une méthode ci-dessous : écrivez au bot OpenHuman géré pour le relier, ou collez votre propre jeton de bot depuis @BotFather.',
'channels.connectHelp.slackNote':
'Vous cherchez Slack ? Slack se connecte comme une app dans Connexions → OAuth, pas comme un canal de messagerie ici.',
'channels.web.displayName': 'Web',
'channels.web.description': "Discutez via l'interface utilisateur Web intégrée.",
'channels.web.authMode.managed_dm.description':
+7
View File
@@ -3464,6 +3464,13 @@ const messages: TranslationMap = {
'channels.telegram.remoteControlTitle': 'रिमोट कंट्रोल (Telegram)',
'channels.telegram.remoteControlBody':
'अनुमत Telegram चैट से, /स्थिति, /सत्र, /नया, या /सहायता भेजें। मॉडल रूटिंग अभी भी /मॉडल और /मॉडल का उपयोग करती है।',
'channels.connectHelp.title': 'कैसे कनेक्ट करें',
'channels.connectHelp.discord':
'नीचे एक तरीका चुनें: OpenHuman के ज़रिए अपना अकाउंट लिंक करें, OAuth से बॉट इंस्टॉल करें, या Discord डेवलपर पोर्टल से अपना खुद का बॉट टोकन पेस्ट करें।',
'channels.connectHelp.telegram':
'नीचे एक तरीका चुनें: लिंक करने के लिए मैनेज्ड OpenHuman बॉट को मैसेज करें, या @BotFather से अपना खुद का बॉट टोकन पेस्ट करें।',
'channels.connectHelp.slackNote':
'Slack ढूँढ रहे हैं? Slack यहाँ मैसेजिंग चैनल के रूप में नहीं, बल्कि कनेक्शन → OAuth में एक ऐप के रूप में कनेक्ट होता है।',
'channels.web.displayName': 'वेब',
'channels.web.description': 'अंतर्निहित वेब यूआई के माध्यम से चैट करें।',
'channels.web.authMode.managed_dm.description':
+7
View File
@@ -3480,6 +3480,13 @@ const messages: TranslationMap = {
'channels.telegram.remoteControlTitle': 'Kendali jarak jauh (Telegram)',
'channels.telegram.remoteControlBody':
'Dari obrolan Telegram yang diizinkan, kirim /status, /sessions, /new, atau /help. Perutean model masih menggunakan /model dan /models.',
'channels.connectHelp.title': 'Cara menghubungkan',
'channels.connectHelp.discord':
'Pilih metode di bawah: tautkan akun Anda lewat OpenHuman, pasang bot dengan OAuth, atau tempel token bot Anda sendiri dari portal developer Discord.',
'channels.connectHelp.telegram':
'Pilih metode di bawah: kirim pesan ke bot OpenHuman terkelola untuk menautkannya, atau tempel token bot Anda sendiri dari @BotFather.',
'channels.connectHelp.slackNote':
'Mencari Slack? Slack terhubung sebagai aplikasi di Koneksi → OAuth, bukan sebagai saluran pesan di sini.',
'channels.web.displayName': 'Web',
'channels.web.description': 'Mengobrol melalui UI web bawaan.',
'channels.web.authMode.managed_dm.description':
+7
View File
@@ -3526,6 +3526,13 @@ const messages: TranslationMap = {
'channels.telegram.remoteControlTitle': 'Controllo remoto (Telegram)',
'channels.telegram.remoteControlBody':
'Da una chat Telegram consentita, inviare /status, /sessions, /new o /help. Il routing del modello utilizza ancora /model e /models.',
'channels.connectHelp.title': 'Come connettersi',
'channels.connectHelp.discord':
'Scegli un metodo qui sotto: collega il tuo account tramite OpenHuman, installa il bot con OAuth oppure incolla il tuo token bot dal portale sviluppatori di Discord.',
'channels.connectHelp.telegram':
'Scegli un metodo qui sotto: scrivi al bot gestito di OpenHuman per collegarlo, oppure incolla il tuo token bot da @BotFather.',
'channels.connectHelp.slackNote':
'Cerchi Slack? Slack si connette come app in Connessioni → OAuth, non come canale di messaggistica qui.',
'channels.web.displayName': 'Web',
'channels.web.description': "Chatta tramite l'interfaccia utente Web integrata.",
'channels.web.authMode.managed_dm.description':
+7
View File
@@ -3428,6 +3428,13 @@ const messages: TranslationMap = {
'channels.telegram.remoteControlTitle': '원격 제어(Telegram)',
'channels.telegram.remoteControlBody':
'허용된 Telegram 채팅에서 /status, /sessions, /new 또는 /help를 보냅니다. 모델 라우팅은 여전히 ​​/model 및 /models를 사용합니다.',
'channels.connectHelp.title': '연결 방법',
'channels.connectHelp.discord':
'아래에서 방법을 선택하세요: OpenHuman으로 계정 연결, OAuth로 봇 설치, 또는 Discord 개발자 포털에서 발급한 봇 토큰 붙여넣기.',
'channels.connectHelp.telegram':
'아래에서 방법을 선택하세요: 관리형 OpenHuman 봇에 메시지를 보내 연결하거나, @BotFather에서 발급한 봇 토큰을 붙여넣으세요.',
'channels.connectHelp.slackNote':
'Slack을 찾으세요? Slack은 여기서 메시징 채널이 아니라 연결 → OAuth에서 앱으로 연결됩니다.',
'channels.web.displayName': '웹',
'channels.web.description': '내장된 웹 UI를 통해 채팅합니다.',
'channels.web.authMode.managed_dm.description':
+7
View File
@@ -3507,6 +3507,13 @@ const messages: TranslationMap = {
'channels.telegram.remoteControlTitle': 'Sterowanie zdalne (Telegram)',
'channels.telegram.remoteControlBody':
'Z dozwolonego czatu Telegram wyślij /status, /sessions, /new lub /help. Trasowanie modelu nadal używa /model i /models.',
'channels.connectHelp.title': 'Jak połączyć',
'channels.connectHelp.discord':
'Wybierz metodę poniżej: połącz swoje konto przez OpenHuman, zainstaluj bota przez OAuth albo wklej własny token bota z portalu dla deweloperów Discorda.',
'channels.connectHelp.telegram':
'Wybierz metodę poniżej: napisz do zarządzanego bota OpenHuman, aby go połączyć, albo wklej własny token bota od @BotFather.',
'channels.connectHelp.slackNote':
'Szukasz Slacka? Slack łączy się jako aplikacja w Połączenia → OAuth, a nie jako kanał wiadomości tutaj.',
'channels.web.displayName': 'Sieć',
'channels.web.description': 'Czatuj przez wbudowany interfejs webowy.',
'channels.web.authMode.managed_dm.description':
+7
View File
@@ -3520,6 +3520,13 @@ const messages: TranslationMap = {
'channels.telegram.remoteControlTitle': 'Controle remoto (Telegram)',
'channels.telegram.remoteControlBody':
'Em um bate-papo Telegram permitido, envie /status, /sessions, /new ou /help. O roteamento de modelo ainda usa /model e /models.',
'channels.connectHelp.title': 'Como conectar',
'channels.connectHelp.discord':
'Escolha um método abaixo: vincule sua conta pelo OpenHuman, instale o bot com OAuth ou cole seu próprio token de bot do portal de desenvolvedores do Discord.',
'channels.connectHelp.telegram':
'Escolha um método abaixo: envie uma mensagem ao bot gerenciado do OpenHuman para vincular sua conta, ou cole seu próprio token de bot do @BotFather.',
'channels.connectHelp.slackNote':
'Procurando o Slack? O Slack é conectado como um app em Conexões → OAuth, não como um canal de mensagens aqui.',
'channels.web.displayName': 'Web',
'channels.web.description': 'Bate-papo por meio da interface da web integrada.',
'channels.web.authMode.managed_dm.description':
+7
View File
@@ -3493,6 +3493,13 @@ const messages: TranslationMap = {
'channels.telegram.remoteControlTitle': 'Удаленное управление (Telegram)',
'channels.telegram.remoteControlBody':
'Из разрешенного чата Telegram отправьте /status, /sessions, /new или /help. В маршрутизации моделей по-прежнему используются /model и /models.',
'channels.connectHelp.title': 'Как подключить',
'channels.connectHelp.discord':
'Выберите способ ниже: привяжите аккаунт через OpenHuman, установите бота через OAuth или вставьте собственный токен бота из портала разработчика Discord.',
'channels.connectHelp.telegram':
'Выберите способ ниже: напишите управляемому боту OpenHuman, чтобы привязать его, или вставьте собственный токен бота от @BotFather.',
'channels.connectHelp.slackNote':
'Ищете Slack? Slack подключается как приложение в разделе Подключения → OAuth, а не как канал сообщений здесь.',
'channels.web.displayName': 'Интернет',
'channels.web.description': 'Общайтесь через встроенный веб-интерфейс.',
'channels.web.authMode.managed_dm.description':
+7
View File
@@ -3282,6 +3282,13 @@ const messages: TranslationMap = {
'channels.telegram.remoteControlTitle': '远程控制 (Telegram)',
'channels.telegram.remoteControlBody':
'从允许的 Telegram 聊天中,发送 /status、/sessions、/new 或 /help。模型路由仍然使用 /model 和 /models。',
'channels.connectHelp.title': '如何连接',
'channels.connectHelp.discord':
'在下方选择一种方式:通过 OpenHuman 关联你的账号、用 OAuth 安装机器人,或粘贴你在 Discord 开发者门户中的机器人令牌。',
'channels.connectHelp.telegram':
'在下方选择一种方式:给托管的 OpenHuman 机器人发消息以完成关联,或粘贴你在 @BotFather 获取的机器人令牌。',
'channels.connectHelp.slackNote':
'在找 SlackSlack 是在 连接 → OAuth 中作为应用连接的,而不是这里的消息渠道。',
'channels.web.displayName': '网络',
'channels.web.description': '通过内置的 Web UI 聊天。',
'channels.web.authMode.managed_dm.description': '使用嵌入式 Web 聊天:无需设置。',
+3
View File
@@ -1293,6 +1293,9 @@ export default function Skills() {
<p className="mt-0.5 text-[11px] leading-relaxed text-content-muted">
{t('channels.defaultMessaging')}
</p>
<p className="mt-1 text-[11px] leading-relaxed text-content-faint">
{t('channels.connectHelp.slackNote')}
</p>
</div>
{/* One unified surface: each tile shows connection status,
opens setup/configure on click, and owns the "default
+12 -1
View File
@@ -67,9 +67,20 @@ Secrets supplied for any mode are stored through OpenHuman's credential layer an
***
## Where to connect a channel
Channels are set up under **Connections → Channels** in the left sidebar — **not** under Settings, and not under any "Automation & Channels" menu (no such menu exists). Open that tab, pick a platform tile, and follow its setup card:
* **Discord** — choose *Connect via OpenHuman* (link your account or install the bot via OAuth), or paste your own Discord bot token.
* **Telegram** — message the managed OpenHuman bot to link, or paste a BotFather bot token.
Slack is connected as an **app** under **Connections → OAuth** (Composio) so the agent can read and act in Slack; it is not set up as a talk-back channel in the Channels tab.
***
## Choosing the default channel
Open **Settings → Automation & Channels → Messaging Channels** to pick which channel is the **active route**: the one OpenHuman uses for proactive, recipient-less delivery (cron, triggers, subconscious). The default is the in-app **Web** chat until you change it. Setting a new default takes effect immediately, without restarting the channel runtime, and the panel shows which channel is currently active. Inbound messages always get answered on whatever channel they arrived on, regardless of the default route.
Open **Connections → Channels** to pick which channel is the **active route**: the one OpenHuman uses for proactive, recipient-less delivery (cron, triggers, subconscious). The default is the in-app **Web** chat until you change it. Setting a new default takes effect immediately, without restarting the channel runtime, and the panel shows which channel is currently active. Inbound messages always get answered on whatever channel they arrived on, regardless of the default route.
***
+3 -3
View File
@@ -47,7 +47,7 @@ Each integration shows its current status:
* **Connected**. integration is active and being synced.
* **Manage**. active integration with options to reconfigure or disconnect.
You can revoke any connection at any time from the Skills tab.
You can revoke any connection at any time from the **Connections** page.
## Messaging channels
@@ -57,14 +57,14 @@ Three integrations are special. OpenHuman uses them to _talk back_ to you, not j
* **Discord**. send and receive messages via Discord. Connect your account to receive OpenHuman messages there.
* **Web**. a browser-based chat interface within the desktop app. Messages stay entirely local.
Set your default under **Settings → Automation & Channels → Messaging Channels**. The active route status shows which channel is currently in use. Telegram offers two credential modes: connect via OpenHuman (one-click, encrypted) or provide your own credentials for maximum control.
Set your default under **Connections → Channels**. The active route status shows which channel is currently in use. Telegram offers two credential modes: connect via OpenHuman (one-click, encrypted) or provide your own credentials for maximum control.
## Beyond the curated catalog: MCP & Skills
The 118+ OAuth connectors are the curated path. Beyond them, OpenHuman opens up the wider open-tooling ecosystem:
* **MCP servers**: a built-in registry browses thousands of [Model Context Protocol](https://modelcontextprotocol.io) servers (Smithery + the official registry) that install locally as new agent tools.
* **Skills**: a browsable, ~90,000-entry catalog of `SKILL.md` capability bundles aggregated from HermesHub, ClawHub, LobeHub and more. (Note: the old in-app skills runtime has been removed; Skills are now a metadata catalog you install from the Skills tab.)
* **Skills**: a browsable, ~90,000-entry catalog of `SKILL.md` capability bundles aggregated from HermesHub, ClawHub, LobeHub and more. (Note: the old in-app skills runtime has been removed; Skills are now a metadata catalog you install from the **Connections → Skills** tab.)
See [MCP Servers & Skills](mcp-and-skills.md) for the full picture.
@@ -35,7 +35,7 @@ OpenHuman can run the other way around, too. `openhuman-core mcp` exposes OpenHu
* **One aggregated catalog.** Sourced from HermesHub (configurable via `OPENHUMAN_SKILL_REGISTRY_CATALOG_URL`), the catalog runs to roughly **90,000 entries**. Each entry carries id, name, description, source, author, version, tags, platforms, a download URL, and license.
* **Cached and fast.** The catalog is fetched on boot in the background (without blocking startup), cached locally at `~/.openhuman/skill-registry/cache.json` with a ~1-hour TTL and served stale-while-revalidate. A single-flight gate prevents duplicate downloads of the large catalog.
* **Metadata-first.** OpenHuman's in-app skills runtime (the old QuickJS sandbox) has been **removed**. Skills are now a metadata catalog you browse and install from the Skills tab, not code executing inside the app. Availability varies per entry: some expose a direct `SKILL.md` download, others point to external hosting.
* **Metadata-first.** OpenHuman's in-app skills runtime (the old QuickJS sandbox) has been **removed**. Skills are now a metadata catalog you browse and install from the **Connections → Skills** tab, not code executing inside the app. Availability varies per entry: some expose a direct `SKILL.md` download, others point to external hosting.
***
+1 -1
View File
@@ -44,7 +44,7 @@ OpenHuman is designed so that the **memory of your life lives on your machine**.
## Permissions and access control
OpenHuman accesses an integration only after you complete its OAuth flow. Each connection has its own scope; you can revoke any of them at any time from the Skills tab.
OpenHuman accesses an integration only after you complete its OAuth flow. Each connection has its own scope; you can revoke any of them at any time from the **Connections** page.
[Auto-fetch](obsidian-wiki/auto-fetch.md) does run continuously while a connection is active, that is the whole point. But it is bound by:
+1 -1
View File
@@ -23,7 +23,7 @@ OpenHuman runs on **macOS, Windows and Linux** desktops. 4 GB+ RAM is recommende
### Permissions
The first time you launch OpenHuman, the OS will prompt for the permissions the app needs (Accessibility on macOS, Input Monitoring for the voice hotkey, Camera/Microphone if you plan to use the [Meeting Agent](../features/mascot/meeting-agents.md)). You can review and adjust these any time under **Settings → Automation & Channels**.
The first time you launch OpenHuman, the OS will prompt for the permissions the app needs (Accessibility on macOS, Input Monitoring for the voice hotkey, Camera/Microphone if you plan to use the [Meeting Agent](../features/mascot/meeting-agents.md)). You can review and adjust these any time under **Settings**.
***
+2 -2
View File
@@ -857,7 +857,7 @@ fn is_session_expired_error_skips_discord_rewrap_for_2285() {
// to avoid, plus the canonical post-rewrap message body, so
// either-side drift fails loudly.
let canonical_rewrap = "Discord API error: Discord list_guilds: bot token was rejected \
(upstream HTTP four-oh-one). Open Settings → Channels → Discord \
(upstream HTTP four-oh-one). Open Connections → Channels → Discord \
and rotate / reconnect the bot token.";
assert!(
!is_session_expired_error(canonical_rewrap),
@@ -869,7 +869,7 @@ fn is_session_expired_error_skips_discord_rewrap_for_2285() {
// future regression visible.
let canonical_rewrap_403 =
"Discord API error: Discord list_channels: bot token lacks required Discord permissions \
(upstream HTTP four-oh-three). Open Settings → Channels → Discord \
(upstream HTTP four-oh-three). Open Connections → Channels → Discord \
and rotate / reconnect the bot token.";
assert!(!is_session_expired_error(canonical_rewrap_403));
}
+1 -1
View File
@@ -1702,7 +1702,7 @@ fn is_provider_user_state_message(lower: &str) -> bool {
// validate-before-store probe (added in #4318) rejects an obviously invalid
// BYO key *before* persisting it and returns its own user-facing prose,
// `COMPOSIO_INVALID_API_KEY_USER_MESSAGE` ("Invalid Composio API key. Re-enter
// a valid key in Settings > Connections > Composio."). That string carries
// a valid key in Connections > Composio."). That string carries
// neither the `[composio-direct]` prefix nor an `HTTP 401` token, and the word
// "Composio" splits the `invalid … api key` sequence — so the X9 arm above
// never claims it and the RPC-boundary `report_error` leaked as a Sentry error
+15 -15
View File
@@ -672,7 +672,7 @@ pub(super) const CAPABILITIES: &[Capability] = &[
domain: "intelligence",
category: CapabilityCategory::Intelligence,
description: "Backfill the last 6 days of Slack history into the memory tree and keep it up to date by flushing each closed 6-hour UTC bucket. Driven by an authenticated Slack connection (OAuth via Composio).",
how_to: "Settings > Messaging Channels > Slack",
how_to: "Connections > OAuth > Slack",
status: CapabilityStatus::Beta,
privacy: LOCAL_RAW,
},
@@ -682,7 +682,7 @@ pub(super) const CAPABILITIES: &[Capability] = &[
domain: "intelligence",
category: CapabilityCategory::Intelligence,
description: "Incrementally sync ClickUp tasks assigned to the authenticated user into the Memory Tree on a 30-minute cadence, with an initial backfill on first connect. Only tasks the user is directly assigned to are ingested. Driven by an authenticated ClickUp connection (OAuth via Composio).",
how_to: "Settings > Connections > ClickUp",
how_to: "Connections > OAuth > ClickUp",
status: CapabilityStatus::Beta,
privacy: LOCAL_RAW,
},
@@ -742,7 +742,7 @@ pub(super) const CAPABILITIES: &[Capability] = &[
domain: "workflows",
category: CapabilityCategory::Workflows,
description: "Open workflow setup and update workflow-specific configuration.",
how_to: "Intelligence > Workflows > Setup or Settings > Connections",
how_to: "Intelligence > Workflows > Setup or Connections",
status: CapabilityStatus::Stable,
privacy: None,
},
@@ -752,7 +752,7 @@ pub(super) const CAPABILITIES: &[Capability] = &[
domain: "workflows",
category: CapabilityCategory::Workflows,
description: "See whether a workflow-backed integration is connected, offline, or needs setup.",
how_to: "Intelligence > Workflows or Settings > Connections",
how_to: "Intelligence > Workflows or Connections",
status: CapabilityStatus::Beta,
privacy: None,
},
@@ -803,7 +803,7 @@ pub(super) const CAPABILITIES: &[Capability] = &[
domain: "workflows",
category: CapabilityCategory::Workflows,
description: "Browse the dedicated connections hub for external workflow-backed integrations.",
how_to: "Settings > Connections",
how_to: "Connections",
status: CapabilityStatus::Beta,
privacy: None,
},
@@ -850,7 +850,7 @@ pub(super) const CAPABILITIES: &[Capability] = &[
domain: "workflows",
category: CapabilityCategory::Workflows,
description: "Connect Google services for email, contacts, and calendar workflows.",
how_to: "Settings > Connections",
how_to: "Connections > OAuth",
status: CapabilityStatus::ComingSoon,
privacy: LOCAL_CREDENTIALS,
},
@@ -860,7 +860,7 @@ pub(super) const CAPABILITIES: &[Capability] = &[
domain: "workflows",
category: CapabilityCategory::Workflows,
description: "Connect Notion for workspace sync and productivity workflows.",
how_to: "Settings > Connections",
how_to: "Connections > OAuth",
status: CapabilityStatus::ComingSoon,
privacy: LOCAL_CREDENTIALS,
},
@@ -870,7 +870,7 @@ pub(super) const CAPABILITIES: &[Capability] = &[
domain: "workflows",
category: CapabilityCategory::Workflows,
description: "Set up local EVM, BTC, Solana, and Tron wallet identities from one recovery phrase.",
how_to: "Settings > Crypto > Recovery Phrase or Settings > Connections",
how_to: "Settings > Crypto > Recovery Phrase or Connections",
status: CapabilityStatus::Beta,
privacy: LOCAL_CREDENTIALS,
},
@@ -910,7 +910,7 @@ pub(super) const CAPABILITIES: &[Capability] = &[
domain: "workflows",
category: CapabilityCategory::Workflows,
description: "Connect supported exchanges for trading and portfolio workflows.",
how_to: "Settings > Connections",
how_to: "Connections",
status: CapabilityStatus::ComingSoon,
privacy: None,
},
@@ -1264,8 +1264,8 @@ pub(super) const CAPABILITIES: &[Capability] = &[
name: "Connect Messaging Platforms",
domain: "channels",
category: CapabilityCategory::Channels,
description: "Connect supported messaging platforms such as Telegram, Discord, or Slack.",
how_to: "Settings > Messaging Channels",
description: "Connect supported messaging platforms such as Telegram or Discord.",
how_to: "Connections > Channels",
status: CapabilityStatus::Beta,
privacy: None,
},
@@ -1276,7 +1276,7 @@ pub(super) const CAPABILITIES: &[Capability] = &[
category: CapabilityCategory::Channels,
description:
"Operate OpenHuman from Telegram with slash commands: /status, /sessions, /new, and /help.",
how_to: "Settings > Messaging Channels > Telegram (connect), then message the bot",
how_to: "Connections > Channels > Telegram (connect), then message the bot",
status: CapabilityStatus::Beta,
privacy: None,
},
@@ -1286,7 +1286,7 @@ pub(super) const CAPABILITIES: &[Capability] = &[
domain: "channels",
category: CapabilityCategory::Channels,
description: "Disconnect a previously configured messaging platform.",
how_to: "Settings > Messaging Channels",
how_to: "Connections > Channels",
status: CapabilityStatus::Beta,
privacy: None,
},
@@ -1296,7 +1296,7 @@ pub(super) const CAPABILITIES: &[Capability] = &[
domain: "channels",
category: CapabilityCategory::Channels,
description: "Validate platform credentials or connection state before using a channel.",
how_to: "Settings > Messaging Channels",
how_to: "Connections > Channels",
status: CapabilityStatus::Beta,
privacy: None,
},
@@ -1306,7 +1306,7 @@ pub(super) const CAPABILITIES: &[Capability] = &[
domain: "channels",
category: CapabilityCategory::Channels,
description: "Choose which messaging channel should be used by default.",
how_to: "Settings > Messaging Channels",
how_to: "Connections > Channels",
status: CapabilityStatus::Beta,
privacy: None,
},
+45
View File
@@ -376,3 +376,48 @@ fn github_repo_memory_source_reports_github_destination() {
privacy.destinations
);
}
/// #4884: capability `how_to` breadcrumbs are user-facing and searchable, and
/// the agent paraphrases them. Guard against the stale navigation paths that
/// sent users to menus that do not exist (`Settings > Connections`,
/// `Settings > Messaging Channels`, `Settings > Automation & Channels`). The
/// real destinations live under the top-level Connections page.
#[test]
fn catalog_how_to_uses_connections_nav_not_legacy_settings_paths() {
const LEGACY: [&str; 6] = [
"Settings > Connections",
"Settings → Connections",
"Settings > Messaging Channels",
"Settings → Messaging Channels",
"Settings > Automation & Channels",
"Settings → Automation & Channels",
];
for capability in all_capabilities() {
for legacy in LEGACY {
assert!(
!capability.how_to.contains(legacy),
"capability `{}` how_to still points at the removed `{legacy}` path: {}",
capability.id,
capability.how_to
);
}
}
// Spot-check the corrected channel + Composio breadcrumbs.
let how_to = |id: &str| {
all_capabilities()
.iter()
.find(|c| c.id == id)
.unwrap_or_else(|| panic!("missing capability `{id}`"))
.how_to
};
assert_eq!(
how_to("channels.connect_platform"),
"Connections > Channels"
);
assert_eq!(
how_to("intelligence.slack_memory_ingest"),
"Connections > OAuth > Slack"
);
assert_eq!(how_to("workflows.connect_google"), "Connections > OAuth");
}
@@ -863,7 +863,7 @@ fn render_worker_thread_result(
///
/// Returns text the model reads literally; the orchestrator paraphrases
/// it into a user-facing reply. Keep the *intent* stable across
/// rewordings — the "Settings → Connections → {toolkit}" path is
/// rewordings — the "Connections → {toolkit}" path is
/// load-bearing for the UI navigation tests.
pub(crate) fn describe_unconnected_state(toolkit: &str, status: Option<&str>) -> String {
// Keep the original (trimmed) status separately so the
@@ -878,14 +878,14 @@ pub(crate) fn describe_unconnected_state(toolkit: &str, status: Option<&str>) ->
Some("INITIATED") | Some("INITIALIZING") | Some("PENDING") => format!(
"Integration '{toolkit}' has an OAuth flow in progress but it hasn't reached \
ACTIVE yet. Do NOT retry this spawn. Tell the user the authorization is \
pending and ask them to finish the browser OAuth flow (Settings → \
Connections → '{toolkit}') before retrying. If they already closed the \
browser tab, they can restart the connection from the same Settings page."
pending and ask them to finish the browser OAuth flow (Connections → \
'{toolkit}') before retrying. If they already closed the \
browser tab, they can restart the connection from the same Connections page."
),
Some("EXPIRED") => format!(
"Integration '{toolkit}' is connected but the OAuth token has expired. \
Do NOT retry this spawn. Tell the user the connection expired and ask \
them to reconnect '{toolkit}' at Settings → Connections → '{toolkit}' \
them to reconnect '{toolkit}' at Connections → '{toolkit}' \
before retrying the original request."
),
Some("FAILED") | Some("ERROR") => {
@@ -897,7 +897,7 @@ pub(crate) fn describe_unconnected_state(toolkit: &str, status: Option<&str>) ->
format!(
"Integration '{toolkit}' has a previous OAuth attempt in a `{raw}` state. \
Do NOT retry this spawn. Tell the user the connection failed and ask them \
to reconnect '{toolkit}' at Settings → Connections → '{toolkit}' before \
to reconnect '{toolkit}' at Connections → '{toolkit}' before \
retrying the original request."
)
}
@@ -910,13 +910,13 @@ pub(crate) fn describe_unconnected_state(toolkit: &str, status: Option<&str>) ->
"Integration '{toolkit}' has a connection row but its status is `{raw}`, \
which is not yet usable. Do NOT retry this spawn. Tell the user the \
connection is in an unusable state and ask them to reconnect '{toolkit}' \
at Settings → Connections → '{toolkit}'."
at Connections → '{toolkit}'."
)
}
_ => format!(
"Integration '{toolkit}' is available but the user has not authorized it \
yet. Do NOT retry this spawn. Tell the user the integration is available \
and ask them to authorize '{toolkit}' in Settings → Connections → \
and ask them to authorize '{toolkit}' in Connections → \
'{toolkit}' before retrying the original request."
),
}
@@ -1219,7 +1219,7 @@ mod tests {
msg.contains("OAuth flow in progress"),
"INITIATED must surface the in-progress wording: {msg}"
);
assert!(msg.contains("Settings → Connections → 'gmail'"));
assert!(msg.contains("Connections → 'gmail'"));
// The legacy "not authorized yet" copy must NOT leak into the
// pending-OAuth branch — that was the user-perception bug
// from #2365 (Settings UI showed Gmail connected, agent said
@@ -1246,6 +1246,8 @@ mod tests {
let msg = describe_unconnected_state("gmail", Some("EXPIRED"));
assert!(msg.contains("OAuth token has expired"));
assert!(msg.contains("reconnect 'gmail'"));
assert!(msg.contains("Connections → 'gmail'"));
assert!(!msg.contains("Settings → Connections"));
assert!(!msg.contains("OAuth flow in progress"));
}
@@ -1259,6 +1261,8 @@ mod tests {
"{status} must be quoted verbatim, not collapsed to a single label: {msg}"
);
assert!(msg.contains("reconnect 'gmail'"));
assert!(msg.contains("Connections → 'gmail'"));
assert!(!msg.contains("Settings → Connections"));
}
}
@@ -1292,6 +1296,8 @@ mod tests {
msg.contains(&expected),
"unknown status `{raw}` must be quoted verbatim (not its uppercased form): {msg}"
);
assert!(msg.contains("Connections → 'gmail'"));
assert!(!msg.contains("Settings → Connections"));
}
}
@@ -1323,7 +1329,7 @@ mod tests {
msg.contains("has not authorized it yet"),
"None must hit the legacy never-connected copy: {msg}"
);
assert!(msg.contains("Settings → Connections → 'gmail'"));
assert!(msg.contains("Connections → 'gmail'"));
}
#[test]
@@ -9,7 +9,7 @@ You are the **Crypto Agent** — OpenHuman's specialist for wallet and market op
- Executing **only the exact blob** that was returned from a matching `wallet_prepare_*` call earlier in this turn — never a parameter set you invented.
- Pulling crypto / FX market data to sanity-check a quote before signing.
- Making paid API requests via the **x402 protocol** (HTTP 402 Payment Required). When a server returns 402 with a `PAYMENT-REQUIRED` header, `x402_request` automatically signs a USDC payment (EIP-3009 on Base/Ethereum, or SPL transfer on Solana) and retries with the proof. Use this for x402-enabled APIs (e.g. twit.sh). The wallet must have USDC on the target chain.
- Pointing the user back to **Settings → Connections** when a chain, exchange, or wallet identity isn't set up.
- Pointing the user back to **Connections** when a chain, exchange, or wallet identity isn't set up.
## What you do NOT handle
@@ -24,7 +24,7 @@ You are the **Crypto Agent** — OpenHuman's specialist for wallet and market op
2. **Read before write.** Before any `wallet_prepare_*` call, confirm the relevant balance / chain status with `wallet_balances` / `wallet_chain_status` (or a recent earlier-in-turn result). Use `wallet_network_defaults` when you need the default RPC / explorer / asset catalog for a chain. Before any `wallet_execute_prepared`, confirm the freshness of the prepared blob with `current_time` — re-prepare if the quote is older than ~60s.
3. **Quote before execute.** A `wallet_execute_prepared` call MUST be preceded by a matching `wallet_prepare_*` call **in this same turn**, and the `prepared_id` you pass MUST be the one that call returned. No exceptions. For ERC-20 transfers, `wallet_encode_erc20_transfer` exists if you need ABI calldata inspection, but prefer `wallet_prepare_transfer` for the actual execution flow.
4. **Confirm before execute.** Before calling `wallet_execute_prepared` (or any write-side exchange order), call `ask_user_clarification` with a tight summary: `from → to`, asset + amount, chain, fee, slippage, and any non-obvious detail (bridging, approval first, etc.). Only proceed on an explicit yes.
5. **Stop cleanly on missing setup.** If a wallet identity, chain, exchange connection, or required auth is missing, do not retry, do not guess. Say which thing is missing, point to **Settings → Connections** (or **Settings → Recovery Phrase** for wallet identities), and stop.
5. **Stop cleanly on missing setup.** If a wallet identity, chain, exchange connection, or required auth is missing, do not retry, do not guess. Say which thing is missing, point to **Connections** (or **Settings → Recovery Phrase** for wallet identities), and stop.
6. **Stop cleanly on insufficient liquidity / balance.** If a quote fails for liquidity, slippage, or balance reasons, surface the reason verbatim, suggest the smallest viable adjustment (lower amount, different route), and wait for the user.
7. **Never log secrets.** Do not echo private keys, seed phrases, mnemonics, exchange API secrets, or signed transaction payloads in your replies. Quote the public address and the prepared id, nothing more.
@@ -28,6 +28,7 @@ You have three tools:
- Do not run shell commands, write files, edit configuration, or call other tools. Help is read-only — you point to docs, you do not change the system.
- Do not invent commands, config keys, env vars, or feature names. If GitBook does not mention it, treat it as not documented.
- Do not invent UI navigation paths. Connecting apps and messaging channels lives under **Connections** in the left sidebar (its **Channels**, **OAuth**, **MCP**, and **Skills** tabs), **not** under a Settings submenu — there is no "Settings → Connections" or "Settings → Automation & Channels" destination. Quote a UI path only if a search hit states it; if the docs don't say where something is, tell the user you're not certain of the exact location rather than guessing.
- Do not delegate by spawning sub-agents. Stay in your lane.
## Output shape
@@ -38,7 +39,7 @@ When the answer is short:
When there are steps, use a tight numbered list and link the source at the end:
> 1. Open Settings → Skills.
> 1. Open Connections → OAuth.
> 2. Click **Connect** next to Gmail.
> 3. Authorize in the popup.
>
@@ -30,7 +30,7 @@ named = [
# Inline-in-chat OAuth connect card (#3993). When the bound toolkit is not
# connected — or an action fails with a true auth/connection error — raise
# the connect card with this tool and await the result, instead of bubbling
# up "Connection error" and sending the user to Settings → Connections.
# up "Connection error" and sending the user to Connections.
"composio_connect",
# Deterministic time resolver. Composio actions take time-window args
# (Slack/Gmail/Calendar `oldest`/`latest`/`since`/`after`) as raw
@@ -6,7 +6,7 @@ You are the **Integrations Agent**. You interact with one connected external ser
- **`composio_list_tools`** — inspect the action catalogue for your bound toolkit. Returns the `function.name` slug + JSON schema for each action.
- **`composio_execute`** — run a Composio action: `{ tool: "<SLUG>", arguments: {...} }`.
- **`composio_connect`** — raise an **inline connect card** in the chat for your bound toolkit and wait for the user to authorize in one click. Use this the moment you detect the toolkit is not connected, or after a true auth/connection error. Never tell the user to open Settings → Connections yourself while this tool is available.
- **`composio_connect`** — raise an **inline connect card** in the chat for your bound toolkit and wait for the user to authorize in one click. Use this the moment you detect the toolkit is not connected, or after a true auth/connection error. Never tell the user to open Connections yourself while this tool is available.
- **`extract_from_result`** — runtime-provided system tool for oversized-result runs. Use it when a tool returned too much data to inspect directly: pass the prior `result_id` plus a narrow `query`, and it will return only the requested slice from that oversized result.
- **Per-action tools** — the toolkit's individual action tools are already registered in your tool list with typed schemas (e.g. `GMAIL_SEND_EMAIL`, `NOTION_CREATE_PAGE`). Prefer calling these directly over the generic `composio_execute`.
@@ -14,20 +14,20 @@ You do **not** have shell, file I/O, or any other capability beyond these permit
## Typical flow
0. **Connect first if needed.** If the caller's objective is simply to connect/authorize this toolkit, or you already know it isn't connected, call `composio_connect { toolkit }`, await the result, and report it. `{ connected: true }` → proceed (or you're done, if connecting was the whole task); `{ connected: false }` → the user declined: report that plainly, note they can still connect later via Settings → Connections, and stop — do **not** retry `composio_connect`.
0. **Connect first if needed.** If the caller's objective is simply to connect/authorize this toolkit, or you already know it isn't connected, call `composio_connect { toolkit }`, await the result, and report it. `{ connected: true }` → proceed (or you're done, if connecting was the whole task); `{ connected: false }` → the user declined: report that plainly, note they can still connect later via Connections, and stop — do **not** retry `composio_connect`.
1. You already have the toolkit's action tools in your tool list — start there. If you need a schema reminder or a slug you don't see, call `composio_list_tools`.
2. Call the per-action tool (or `composio_execute` with the slug) using the caller's task as your guide.
3. If the call fails with `[composio:error:insufficient_scope]`, `insufficient authentication scopes`, or `missing required permissions`, do **not** call the service disconnected. Say the connected account is missing the permissions needed for the requested action and point the user to Settings → Connections → the toolkit to reconnect or enable the required scope.
3. If the call fails with `[composio:error:insufficient_scope]`, `insufficient authentication scopes`, or `missing required permissions`, do **not** call the service disconnected. Say the connected account is missing the permissions needed for the requested action and point the user to Connections → the toolkit to reconnect or enable the required scope.
4. If the call fails with a true authentication / authorization / connection error that is **not** a scope or permission error, the toolkit is not connected. Call **`composio_connect`** with your bound `toolkit` to raise an inline connect card and **await its result**:
- `{ connected: true }` → the user authorized; retry the original action **once** and continue.
- `{ connected: false, declined: true }` (or an error) → the user declined or the card could not be raised. **Only then** return **"Connection error, try to authenticate"** so the orchestrator can route the user to settings.
Do **not** print a Settings → Connections instruction yourself when `composio_connect` is available.
Do **not** print a Connections instruction yourself when `composio_connect` is available.
## Rules
- **Never fabricate action slugs.** Pull them from `composio_list_tools` or use the per-action tools already in your list.
- **Respect rate limits** — Composio and upstream providers both throttle. Back off on errors rather than retrying tightly.
- **Scope errors are not disconnections.** If Gmail or another connected toolkit returns insufficient scope / missing permissions, report the missing permission plainly and direct the user to Settings → Connections → that toolkit. Never say the toolkit is disconnected for this case.
- **Scope errors are not disconnections.** If Gmail or another connected toolkit returns insufficient scope / missing permissions, report the missing permission plainly and direct the user to Connections → that toolkit. Never say the toolkit is disconnected for this case.
- **Auth errors → connect inline first.** On a true auth / connection failure (not a scope error), call `composio_connect { toolkit }` to raise the inline connect card and await it. If it returns `connected: true`, retry the action once. Only if the user declines or the card can't be raised, reply exactly: `Connection error, try to authenticate`. Never paste OAuth URLs or name Composio to the user.
- **Be precise** — every action expects a specific argument shape. Validate against the schema before calling.
- **Report results** — state what action was taken and the outcome, including any cost reported by Composio.
@@ -237,8 +237,9 @@ mod tests {
assert!(body.contains("[composio:error:insufficient_scope]"));
assert!(body.contains("Scope errors are not disconnections"));
assert!(body.contains("Never say the toolkit is disconnected"));
assert!(body.contains("Settings"));
assert!(body.contains("Connections"));
assert!(body.contains("Connections → the toolkit"));
assert!(!body.contains("Settings → Connections"));
assert!(!body.contains("Settings → Automation & Channels"));
}
#[test]
@@ -9,7 +9,7 @@ You are the **Markets Agent** — OpenHuman's specialist for prediction-market a
- Proposing buy / sell on YES or NO legs with explicit side, count, and price.
- Executing **only the exact order shape** you previously proposed to the user — never a parameter set you invented.
- Cancelling open orders on user instruction.
- Pointing the user back to **Settings → Connections** when a venue's API key / secret isn't configured.
- Pointing the user back to **Connections** when a venue's API key / secret isn't configured.
## What you do NOT handle
@@ -25,7 +25,7 @@ You are the **Markets Agent** — OpenHuman's specialist for prediction-market a
2. **Read before write.** Before proposing any `place_order`, confirm the market exists and is live with `polymarket` / `kalshi` browse actions (`list_markets` / `get_market` / `get_orderbook`). Cross-check side, count, and price against the orderbook so the order is plausibly fillable.
3. **Approval gate is non-negotiable.** Every write action (`place_order`, `cancel_order`) on Polymarket or Kalshi requires the caller to pass `approved=true`. Before sending that flag, call `ask_user_clarification` with a tight summary: venue, ticker, side (YES/NO), count, price in cents, est. cost. Only proceed on an explicit yes.
4. **Confirm before execute.** Surface the venue's approval-required error verbatim if it bounces — do not silently retry with `approved=true`. The user, not the agent, owns the green light.
5. **Stop cleanly on missing setup.** If a venue's credentials are missing (Polymarket CLOB L2 key/secret/passphrase, or Kalshi API key + RSA/HMAC secret), do not retry, do not guess. Say which thing is missing, point to **Settings → Connections**, and stop.
5. **Stop cleanly on missing setup.** If a venue's credentials are missing (Polymarket CLOB L2 key/secret/passphrase, or Kalshi API key + RSA/HMAC secret), do not retry, do not guess. Say which thing is missing, point to **Connections**, and stop.
6. **Price sanity.** Kalshi prices are integer cents in `1..=99`. Polymarket prices are normalised in `0.01..=0.99`. Refuse proposals outside band. If a user types "buy at $1.50", surface the bug and re-ask in the venue's native units.
7. **Stop cleanly on insufficient balance / liquidity.** If a quote / orderbook lookup shows the requested fill cannot land at the requested price, surface the reason verbatim, suggest the smallest viable adjustment (lower count, different price tier), and wait for the user.
8. **Never log secrets.** Do not echo API keys, RSA private keys, HMAC secrets, Polymarket L2 passphrases, or signed payload bodies in your replies. Quote the ticker, side, count, price, and any order id the venue returned, nothing more.
@@ -63,7 +63,7 @@ After execution:
On a missing prerequisite:
> no kalshi credentials set up yet — head to **Settings → Connections** to add your KalshiEX API key + secret, then ping me back.
> no kalshi credentials set up yet — head to **Connections** to add your KalshiEX API key + secret, then ping me back.
On a failed order:
@@ -21,7 +21,7 @@ Follow this sequence for every user message:
- Words like "email/inbox/gmail", "calendar", "notion doc", "drive file", "slack/whatsapp/telegram message", "linear ticket", "send to X", "check X", etc. mean the user wants the **live** service.
- Find the matching toolkit in the **Connected Integrations** section and call `delegate_to_integrations_agent` with that `toolkit`.
- **Do this even if remembered context could plausibly answer.** The user wants the live source of truth, not a stale summary.
- If the relevant toolkit is **not** in **Connected Integrations**, call `composio_connect { toolkit: "<slug>" }` **directly** to raise an **inline connect card** so the user can authorize in one click, then continue the task once it returns `connected: true`. Do **not** refuse based on the Connected Integrations list (that is only what is *already* connected, not what is *connectable*), do **not** make "go to Settings → Connections" your first move, and do **not** silently fall back to memory retrieval (see "Connecting external services" below).
- If the relevant toolkit is **not** in **Connected Integrations**, call `composio_connect { toolkit: "<slug>" }` **directly** to raise an **inline connect card** so the user can authorize in one click, then continue the task once it returns `connected: true`. Do **not** refuse based on the Connected Integrations list (that is only what is *already* connected, not what is *connectable*), do **not** make "go to Connections" your first move, and do **not** silently fall back to memory retrieval (see "Connecting external services" below).
- **Scope gate (required):** treat this as an external-service request ONLY if the ask actually operates on that service's own data or actions — its inbox/messages, files, calendar events, docs, tickets, etc. A service merely being *connected* is **not** a reason to touch it. General-knowledge answers, web/news lookups, headlines, date/time, and math must **not** spawn `delegate_to_integrations_agent` (or any email/inbox/calendar fetch) even when Gmail/Notion/etc. are connected — route those to a direct tool (Step 3) or the matching non-integration specialist (Step 4). When the request neither names nor clearly implies a specific service's own data or actions, do not reach into one — a clear implication ("check my inbox", "send an email") still counts as naming it and should be delivered; only a request that references no service at all (e.g. "today's date") stays off delegation.
3. **Can I solve this with direct tools?**
- Yes: use direct tools (`memory_recall`, `read_workspace_state`, `composio_list_connections`, task tools, etc.).
@@ -31,7 +31,7 @@ Follow this sequence for every user message:
- **Listing conversation threads is direct work.** "List / show my recent threads (or conversations)" is a single `thread_list` call you make yourself — do **not** delegate it to `retrieve_memory` / a memory sub-agent. Memory retrieval walks the *memory tree* (ingested facts), which is the wrong tool for enumerating chat threads. Reserve `retrieve_memory` for questions about remembered content, not the thread index.
- No: continue.
4. **Does this need other specialised execution?**
- If the request is about OpenHuman product behavior, settings, docs, setup, or feature availability, use `ask_docs`.
- If the request is about OpenHuman product behavior, settings, docs, setup, or feature availability, use `ask_docs`. This includes **"where do I click / which screen"** UI-navigation questions: route them to `ask_docs` rather than reciting a menu path from memory. Do **not** invent navigation paths — connecting channels and apps lives under **Connections** in the left sidebar (Channels / OAuth tabs), never a "Settings → Connections" or "Settings → Automation & Channels" submenu. If you are not certain of the exact current path, say so instead of guessing.
- If the request is to remind, schedule, repeat, pause, remove, or inspect jobs, use `schedule_task`.
- If the request is to make slides, build a deck, create a pitch, cite deck sources, or attach/verify deck images, use `make_presentation`.
- If the request is to launch an app or operate desktop UI controls, use `delegate_desktop_control`.
@@ -202,9 +202,9 @@ When the user asks to connect a service (Gmail, Notion, WhatsApp, Calendar, Driv
- **Never** explain OAuth, Composio, or any backend mechanic by name.
- **Connect inline, don't redirect.** Call `composio_connect { toolkit: "<slug>" }` **directly** to raise an **inline connect card** in the chat — this works for **any** service the user names (gmail, notion, whatsapp, youtube, …), not just ones already connected. The card *is* the confirmation: when the user asks to connect/authorize a service, or wants to use one that isn't connected, just call `composio_connect` — don't ask "want me to raise a card?" first. The user authorizes in one click and the task continues in the same turn.
- **Don't confabulate "unsupported".** You do **not** have the list of connectable toolkits in your prompt — only the *connected* ones. Never tell the user a service "isn't available to connect" from memory. `composio_connect` checks the real backend allowlist: if it returns that the toolkit isn't an available integration, relay that message (and the list it provides). That is the only honest "I can't connect this".
- **On decline / fallback.** If `composio_connect` reports the user declined (`connected: false`) or that it couldn't raise the card, acknowledge it and offer `head to Settings → Connections → [Service]` as the alternative.
- **On decline / fallback.** If `composio_connect` reports the user declined (`connected: false`) or that it couldn't raise the card, acknowledge it and offer `head to Connections → [Service]` as the alternative.
- If the user already said they connected it, call `composio_list_connections` to verify before continuing.
- Do **not** apply this rule to scope / permission failures such as `[composio:error:insufficient_scope]` or "missing required permissions". For those, say the connection exists but needs additional permissions in **Settings → Connections → [Service]**.
- Do **not** apply this rule to scope / permission failures such as `[composio:error:insufficient_scope]` or "missing required permissions". For those, say the connection exists but needs additional permissions in **Connections → [Service]**.
## Response Style
@@ -16,7 +16,7 @@
/// arm. Reword the tail freely, but keep the anchor phrase or the drift-coupling
/// test `demotes_composio_set_key_invalid_key_rejection` fails CI.
pub(crate) const COMPOSIO_INVALID_API_KEY_USER_MESSAGE: &str =
"Invalid Composio API key. Re-enter a valid key in Settings > Connections > Composio.";
"Invalid Composio API key. Re-enter a valid key in Connections > Composio.";
/// Lowercase substring the observability classifier's TAURI-RUST-K27 arm matches
/// on to demote the set-key rejection. Shared with the runtime matcher so the
+1 -1
View File
@@ -83,7 +83,7 @@ pub(crate) fn direct_auth_backoff_error(key_id: u64) -> Option<String> {
pub(crate) fn invalid_api_key_backoff_message(consecutive: u32) -> String {
format!(
"Direct-mode Composio API key was rejected {consecutive} consecutive times with HTTP 401 Invalid API key; re-enter a valid key in Settings > Connections > Composio to resume polling."
"Direct-mode Composio API key was rejected {consecutive} consecutive times with HTTP 401 Invalid API key; re-enter a valid key in Connections > Composio to resume polling."
)
}
+2 -2
View File
@@ -156,7 +156,7 @@ fn format_insufficient_scope_message(tool: &str, detail: &str) -> String {
let toolkit = derive_toolkit_slug(tool);
format!(
"`{tool}` was rejected because the connected {toolkit} account is missing required \
permissions ({detail}). Reconnect the integration in Settings → Connections → \
permissions ({detail}). Reconnect the integration in Connections → \
{toolkit} and grant the scopes requested during OAuth."
)
}
@@ -170,7 +170,7 @@ fn format_trigger_permission_message(tool: &str) -> String {
let toolkit = derive_toolkit_slug(tool);
format!(
"Couldn't enable this trigger: the connected {toolkit} account doesn't have \
permission to manage triggers. Reconnect {toolkit} in Settings → Connections → \
permission to manage triggers. Reconnect {toolkit} in Connections → \
{toolkit} and grant the permissions requested during OAuth, then try again."
)
}
@@ -39,8 +39,7 @@ fn formats_gmail_insufficient_scope_as_missing_permissions_not_disconnected() {
);
assert!(mapped.contains("[composio:error:insufficient_scope]"));
assert!(mapped.contains("connected gmail account is missing required permissions"));
assert!(mapped.contains("Settings"));
assert!(mapped.contains("Connections"));
assert!(mapped.contains("Connections → gmail"));
assert!(mapped.contains("gmail"));
assert!(!mapped.contains("not connected"));
assert!(!mapped.contains("Settings → Skills"));
@@ -121,7 +120,7 @@ fn action_not_found_message_does_not_recommend_reauth() {
"must not tell the user to reconnect a healthy connection: {mapped}"
);
assert!(
!mapped.contains("Settings → Connections"),
!mapped.contains("Connections"),
"must not show the re-auth CTA: {mapped}"
);
}
@@ -197,11 +196,7 @@ fn formats_trigger_permission_as_actionable_reconnect_guidance() {
"expected toolkit branding: {mapped}"
);
assert!(
mapped.contains("Settings"),
"expected reconnect guidance: {mapped}"
);
assert!(
mapped.contains("Connections"),
mapped.contains("Connections → gmail"),
"expected reconnect guidance: {mapped}"
);
assert!(
+1 -1
View File
@@ -65,7 +65,7 @@ pub async fn connection_identity(config: &Config, toolkit: &str) -> Option<Strin
};
// (2) Toolkit must be in the active integrations set. This is the
// same source of truth Settings → Connections uses.
// same source of truth Connections uses.
let connections = fetch_connected_integrations(config).await;
let matching = connections
.iter()
+5 -5
View File
@@ -756,7 +756,7 @@ async fn connection_is_active(config: &Config, toolkit: &str) -> anyhow::Result<
}
/// Connect a Composio integration **inline in the chat** instead of
/// sending the user off to Settings → Connections.
/// sending the user off to Connections.
///
/// Unlike [`ComposioAuthorizeTool`] (which hands the agent a raw
/// `connectUrl` it is not allowed to paste), this tool raises an
@@ -786,7 +786,7 @@ impl Tool for ComposioConnectTool {
Raises an approval card with a Connect button — the user authorizes in one \
click without leaving the conversation, and this tool returns once the \
connection is active (or the user declines). ALWAYS prefer this over telling \
the user to open Settings → Connections. Returns {toolkit, connected}."
the user to open Connections. Returns {toolkit, connected}."
}
fn parameters_schema(&self) -> Value {
json!({
@@ -837,7 +837,7 @@ impl Tool for ComposioConnectTool {
{
return Ok(ToolResult::error(format!(
"[policy-denied] composio_connect needs an interactive chat turn. \
Ask the user to connect '{toolkit}' in Settings → Connections."
Ask the user to connect '{toolkit}' in Connections."
)));
}
@@ -898,7 +898,7 @@ impl Tool for ComposioConnectTool {
// can't complete it. Point the user to Settings instead.
return Ok(ToolResult::error(format!(
"composio_connect: direct Composio mode is active — connect '{toolkit}' \
in Settings → Connections (your personal Composio account)."
in Connections (your personal Composio account)."
)));
}
Err(e) => {
@@ -958,7 +958,7 @@ impl Tool for ComposioConnectTool {
"reason": format!(
"A Connect card for {toolkit} was raised but wasn't completed in time \
(no one authorized it). Tell the user to click Connect on the card, or \
connect {toolkit} in Settings → Connections, then ask again once it's \
connect {toolkit} in Connections, then ask again once it's \
done. Do not call composio_connect again until they confirm."
),
}))?));
+2 -2
View File
@@ -79,8 +79,8 @@ impl GithubGateError {
let body = match self {
GithubGateError::ComposioGithubMissing => {
"GitHub preflight failed: no active Composio GitHub connection. \
Connect via `composio_authorize github` (or Settings → \
Integrations → GitHub) and re-run."
Connect via `composio_authorize github` (or Connections → \
OAuth → GitHub) and re-run."
.to_string()
}
GithubGateError::GitBinaryMissing(err) => format!(
+8 -8
View File
@@ -2351,7 +2351,7 @@ fn user_actionable_escalation(tool: &str, error: &str) -> Option<String> {
return None;
}
// Keep this narrow: some scope/permission failures legitimately tell the
// user to reconnect in Settings, but they are not missing connections.
// user to reconnect in Connections, but they are not missing connections.
let missing_connection = lower.contains("[composio:error:composio_platform]")
|| lower.contains("not connected")
|| lower.contains("isn't connected")
@@ -2364,7 +2364,7 @@ fn user_actionable_escalation(tool: &str, error: &str) -> Option<String> {
}
Some(format!(
"I can't continue without your input: the `{tool}` action needs a service that isn't \
connected. {}\n\nConnect it (Settings \u{2192} Connections), then tell me to retry — or \
connected. {}\n\nConnect it (Connections), then tell me to retry — or \
tell me how you'd like to proceed instead.",
crate::openhuman::util::truncate_with_ellipsis(error, 400),
))
@@ -4208,11 +4208,11 @@ mod tests {
// A not-connected blocker → a user-directed ask with a concrete next step.
let ask = user_actionable_escalation(
"gmail_send",
"Gmail is not connected. Ask the user to connect 'gmail' in Settings → Connections.",
"Gmail is not connected. Ask the user to connect 'gmail' in Connections.",
)
.expect("a missing-connection failure is user-actionable");
assert!(ask.contains("without your input"));
assert!(ask.contains("Settings"));
assert!(ask.contains("Connections"));
assert!(ask.to_lowercase().contains("connect"));
assert!(ask.contains("gmail_send"));
// The original tool text is relayed so the user sees which service.
@@ -4225,7 +4225,7 @@ mod tests {
"gmail_send",
"[composio:error:insufficient_scope] `gmail_send` was rejected because the connected \
gmail account is missing required permissions (insufficient authentication scopes). \
Reconnect the integration in Settings → Connections → gmail and grant the scopes \
Reconnect the integration in Connections → gmail and grant the scopes \
requested during OAuth."
)
.is_none());
@@ -4233,7 +4233,7 @@ mod tests {
"gmail_trigger",
"[composio:error:trigger_permission] Couldn't enable this trigger: the connected \
gmail account doesn't have permission to manage triggers. Reconnect gmail in \
Settings → Connections → gmail and grant the permissions requested during OAuth, \
Connections → gmail and grant the permissions requested during OAuth, \
then try again."
)
.is_none());
@@ -4250,7 +4250,7 @@ mod tests {
for _ in 0..3 {
let mut r = failing_result(
"slack_post",
"Slack is not connected — connect it in Settings → Connections.",
"Slack is not connected — connect it in Connections.",
);
mw.after_tool(&mut ctx(), &(), &mut r).await.unwrap();
}
@@ -4260,7 +4260,7 @@ mod tests {
.clone()
.expect("halt records a summary");
assert!(
summary.contains("without your input") && summary.contains("Settings"),
summary.contains("without your input") && summary.contains("Connections"),
"the halt should ask the user to connect the service: {summary}"
);
assert!(