composio-integration-architecture.md

Composio Integration Architecture: Triggers, API Connections & White-Label Auth

Status: Architecture Proposal (Draft) Author: bot0 team Date: 2026-02-24 Dependencies: Daemon tool system, Proxy security model, Proactive engine, Hub relay, Cron scheduler


Table of Contents

  1. Executive Summary
  2. Why Composio
  3. Architecture Overview
  4. Component Design
  5. Security Model
  6. Data Model
  7. White-Label Authentication
  8. Trigger Architecture (Deep Dive)
  9. Multi-Account Management
  10. Permission System
  11. Implementation Plan
  12. Open Questions

Executive Summary

This document proposes integrating Composio into bot0 to unlock:

  1. 10,000+ API connections — Gmail, Slack, GitHub, Salesforce, HubSpot, Google Drive, Notion, Linear, Jira, and thousands more via pre-built toolkits
  2. Triggers — Real-time event subscriptions (webhooks + polling) that feed into the proactive engine, making the agent truly reactive to the world
  3. White-label OAuth — Users authenticate with services through bot0-branded flows, never seeing Composio's domain
  4. Multi-account support — Users can connect multiple accounts per service (work Gmail + personal Gmail)

Core principle: Composio is an infrastructure layer. The user never knows it exists. They see bot0 connecting to their services, bot0-branded OAuth screens, and bot0 managing their connections. Composio is the plumbing.

Security principle: The Composio API key lives on the Proxy (Bytespace), never on the daemon. The daemon interacts with Composio exclusively through proxied endpoints, maintaining the zero-secrets-on-daemon guarantee.


Why Composio

The Alternative: Building It Ourselves

Building OAuth integrations for even 50 services would require:

  • 50+ OAuth app registrations across provider developer portals
  • Token refresh logic per provider (each has quirks)
  • Webhook endpoint management per service
  • Schema maintenance for each API's tool definitions
  • Ongoing maintenance as APIs change

What Composio Provides

CapabilityDetails
Pre-built toolkits500+ apps with tools ready to use
Managed OAuthToken refresh, expiry handling, re-auth prompts
Vercel AI SDK provider@composio/vercel — tools with built-in execute, works directly with streamText()
Trigger infrastructureWebhook + polling triggers with signature verification
MCP supportAlternative integration path via MCP protocol
User isolationPer-user_id credential and tool scoping
White-label authCustom OAuth apps, branded auth screens, custom redirect URIs
SOC 2 Type 2Enterprise-grade security compliance

Pricing Reality

PlanUsersAPI Calls/moCost
Hobby1005K-20KFree
Starter1,000100K$99/mo
Growth5,000500K$199/mo
EnterpriseCustomCustomCustom

For bot0's usage pattern (agent-driven API calls), the Growth plan at $199/mo covers 5,000 users with 500K API calls — extremely cost-effective compared to building and maintaining integrations ourselves.


Architecture Overview

High-Level Data Flow

┌─────────────────────────────────────────────────────────────────────────────┐
│                           DESKTOP / CLI                                      │
│                                                                              │
│  User: "Connect my Google Drive"                                            │
│  Agent: Opens auth URL → User authenticates → Connection saved              │
│                                                                              │
│  User: "Send a Slack message to #general"                                   │
│  Agent: Uses bytespace_execute tool → Proxied to Composio → Slack API        │
│                                                                              │
│  User: "Notify me when I get a GitHub PR review"                            │
│  Agent: Creates trigger → Webhook configured → Events flow to daemon        │
│                                                                              │
└──────────────────────────────┬──────────────────────────────────────────────┘
                               │ IPC
                               ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                            DAEMON                                            │
│                                                                              │
│  ┌──────────────────┐  ┌───────────────────┐  ┌─────────────────────┐      │
│  │ bytespace_search  │  │ bytespace_execute   │  │ bytespace_connect    │      │
│  │                  │  │                    │  │                     │      │
│  │ Discover tools   │  │ Run actions on     │  │ Initiate OAuth for  │      │
│  │ across 500+ apps │  │ connected services │  │ a service           │      │
│  └──────┬───────────┘  └──────┬─────────────┘  └──────┬──────────────┘      │
│         │                     │                        │                     │
│  ┌──────┴─────────────────────┴────────────────────────┴──────────────┐     │
│  │                    ComposioClient (daemon-side)                      │     │
│  │  • Thin wrapper over proxied HTTP                                   │     │
│  │  • Injects session token + device signature                         │     │
│  │  • Routes all calls through Bytespace proxy                         │     │
│  │  • ZERO Composio API key — only session token                       │     │
│  └────────────────────────────┬────────────────────────────────────────┘     │
│                               │                                              │
└───────────────────────────────┼──────────────────────────────────────────────┘
                                │ HTTPS + Device Signature
                                ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                       BYTESPACE PROXY                                        │
│                                                                              │
│  ┌──────────────────────────────────────────────────────────────────┐       │
│  │              /api/proxy/composio/[...path]                        │       │
│  │                                                                   │       │
│  │  1. authenticateProxyRequest() — session + device signature       │       │
│  │  2. Decrypt COMPOSIO_API_KEY from encrypted storage               │       │
│  │  3. Map bot0 userId to Composio userId                            │       │
│  │  4. Forward request to Composio API with x-api-key header         │       │
│  │  5. Return response to daemon                                     │       │
│  └──────────────────────────────────────────────────────────────────┘       │
│                                                                              │
│  ┌──────────────────────────────────────────────────────────────────┐       │
│  │         /api/webhooks/composio (public endpoint)                  │       │
│  │                                                                   │       │
│  │  1. Verify HMAC-SHA256 webhook signature                          │       │
│  │  2. Parse trigger payload (V3 format)                             │       │
│  │  3. Resolve user_id → device_id → target daemon                   │       │
│  │  4. Route trigger event via Hub to daemon                         │       │
│  │  5. OR queue as proactive task if daemon offline                   │       │
│  └──────────────────────────────────────────────────────────────────┘       │
│                                                                              │
└──────────────────────────────┬──────────────────────────────────────────────┘
                               │
                               ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                         COMPOSIO API                                         │
│                                                                              │
│  • Tool discovery & execution                                                │
│  • OAuth flow management                                                     │
│  • Trigger subscription & delivery                                           │
│  • Credential storage & refresh                                              │
│  • User isolation                                                            │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Trigger Event Flow (The Proactive Path)

External Service (Gmail, GitHub, Slack, etc.)
         │
         │ Webhook / Poll result
         ▼
┌─────────────────┐
│    Composio      │  Receives event, matches to trigger instance
│    Backend       │  Delivers to configured webhook URL
└────────┬────────┘
         │ POST /api/webhooks/composio
         │ + HMAC-SHA256 signature
         ▼
┌─────────────────┐
│   Bytespace      │  1. Verify signature
│   Webhook        │  2. Parse V3 payload
│   Handler        │  3. Resolve user → daemon
└────────┬────────┘
         │
    ┌────┴────────────────────────┐
    │                             │
    ▼                             ▼
DAEMON ONLINE               DAEMON OFFLINE
    │                             │
    │ Hub: trigger_event          │ Queue in ctx0_trigger_events
    ▼                             │ (pending, TTL-based)
┌─────────────────┐               │
│    Daemon        │               │ On reconnect:
│                  │               │ Daemon polls pending events
│  Proactive       │               │ Processes in order
│  Engine          │◄──────────────┘
│                  │
│  1. Evaluate     │
│  2. Permission   │
│  3. Execute/Ask  │
└─────────────────┘

Component Design

1. Composio Proxy Layer

Location: apps/bytespace/src/app/api/proxy/composio/[...path]/route.ts

The proxy layer is the single point of contact between daemon and Composio. It follows the exact same pattern as the LLM proxy (/api/proxy/llm/[provider]/[...path]).

typescript
// apps/bytespace/src/app/api/proxy/composio/[...path]/route.ts import { NextRequest, NextResponse } from 'next/server'; import { authenticateProxyRequest, createProxyCorsResponse } from '@/lib/auth/proxy'; const COMPOSIO_API_BASE = 'https://backend.composio.dev/api/v3'; export async function POST( request: NextRequest, { params }: { params: { path: string[] } } ) { const bodyText = await request.text(); // 1. Authenticate: session token + device signature const auth = await authenticateProxyRequest(request, bodyText); if (!auth.success) return auth.response; // 2. Get Composio API key (encrypted on proxy, decrypted here) const composioApiKey = await getComposioApiKey(auth.session.userId); if (!composioApiKey) { return NextResponse.json( { error: 'Composio not configured' }, { status: 400 } ); } // 3. Map bot0 userId to Composio userId // bot0 userId IS the Composio userId (direct mapping) const composioUserId = auth.session.userId; // 4. Forward to Composio API const composioPath = params.path.join('/'); const composioUrl = `${COMPOSIO_API_BASE}/${composioPath}`; const response = await fetch(composioUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': composioApiKey, }, body: bodyText, }); // 5. Stream response back to daemon return new NextResponse(response.body, { status: response.status, headers: { 'Content-Type': response.headers.get('Content-Type') || 'application/json', }, }); } export async function GET( request: NextRequest, { params }: { params: { path: string[] } } ) { // Similar pattern for GET requests (tool discovery, connection status, etc.) // ... } export async function OPTIONS() { return createProxyCorsResponse(); }

Key decisions:

  • bot0 userId = Composio userId — Direct 1:1 mapping. No translation table needed. When a bot0 user with ID usr_abc123 connects to GitHub, that connection is stored under user_id: "usr_abc123" in Composio.
  • Single Composio API key per Bytespace instance — Stored encrypted like other API keys. All users share the same Composio project; isolation is via user_id.
  • Proxy handles all Composio calls — Daemon never sees the Composio API key.

2. Daemon Composio Client

Location: packages/daemon/src/composio/client.ts

A thin HTTP client that routes all Composio operations through the Bytespace proxy.

typescript
// packages/daemon/src/composio/client.ts import { DaemonConfig } from '../config'; import { SigningProvider, createDeviceHeaders } from '../signing'; export interface ComposioClientDeps { getConfig: () => DaemonConfig; getSigningProvider: () => SigningProvider | null; } export class ProxiedComposioClient { constructor(private deps: ComposioClientDeps) {} /** * Search for available tools across Composio toolkits. * Used by the bytespace_search tool. */ async searchTools(query: string, options?: { toolkits?: string[]; limit?: number; }): Promise<ComposioToolInfo[]> { return this.request('POST', 'tools/search', { query, toolkits: options?.toolkits, limit: options?.limit ?? 20, }); } /** * Execute a Composio tool action. * Used by the bytespace_execute tool. */ async executeTool(slug: string, args: Record<string, unknown>): Promise<unknown> { return this.request('POST', 'tools/execute', { slug, arguments: args, }); } /** * List connected accounts for the current user. */ async listConnections(options?: { toolkit?: string; status?: 'ACTIVE' | 'EXPIRED' | 'INITIATED'; }): Promise<ComposioConnection[]> { const params = new URLSearchParams(); if (options?.toolkit) params.set('toolkit', options.toolkit); if (options?.status) params.set('status', options.status); return this.request('GET', `connected_accounts?${params}`); } /** * Initiate an OAuth connection for a toolkit. * Returns a redirect URL the user must visit. */ async initiateConnection(toolkit: string, options?: { authConfigId?: string; callbackUrl?: string; }): Promise<{ redirectUrl: string; connectionId: string }> { return this.request('POST', 'connected_accounts', { toolkit, auth_config_id: options?.authConfigId, callback_url: options?.callbackUrl, }); } /** * Check connection status (polling after OAuth redirect). */ async getConnectionStatus(connectionId: string): Promise<ComposioConnection> { return this.request('GET', `connected_accounts/${connectionId}`); } /** * Create a trigger instance. */ async createTrigger(slug: string, config: Record<string, unknown>): Promise<{ triggerId: string; }> { return this.request('POST', `trigger_instances/${slug}/upsert`, { trigger_config: config, }); } /** * List active trigger instances. */ async listTriggers(): Promise<ComposioTriggerInstance[]> { return this.request('GET', 'trigger_instances/active'); } /** * Enable/disable/delete a trigger. */ async manageTrigger(triggerId: string, action: 'enable' | 'disable' | 'delete'): Promise<void> { await this.request('POST', `trigger_instances/${triggerId}/${action}`); } /** * List available trigger types for a toolkit. */ async listTriggerTypes(toolkit?: string): Promise<ComposioTriggerType[]> { const params = toolkit ? `?toolkit=${toolkit}` : ''; return this.request('GET', `triggers/types${params}`); } // ─── Internal ─── private async request<T>(method: string, path: string, body?: unknown): Promise<T> { const config = this.deps.getConfig(); const signingProvider = this.deps.getSigningProvider(); const url = `${config.proxyUrl}/api/proxy/composio/${path}`; const bodyStr = body ? JSON.stringify(body) : undefined; const headers: Record<string, string> = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${config.sessionToken}`, }; // Inject device signature if (signingProvider && bodyStr) { const deviceHeaders = await createDeviceHeaders(signingProvider, bodyStr); Object.assign(headers, deviceHeaders); } const response = await fetch(url, { method, headers, body: bodyStr }); if (!response.ok) { const error = await response.text(); throw new Error(`Composio proxy error (${response.status}): ${error}`); } return response.json(); } }

3. Tool Integration in Agent Loop

Three daemon tools expose Composio to the agent. These follow the existing tool pattern exactly.

Tool 1: bytespace_search — Discover Available Tools

typescript
// packages/daemon/src/tools/composio-search.ts import { Tool, ToolResult } from './types'; export function createComposioSearchTool(deps: { getClient: () => ProxiedComposioClient | null; }): Tool { return { name: 'bytespace_search', description: `Search for available API tools across 500+ connected services (Gmail, Slack, GitHub, Salesforce, etc.). Use this to discover what actions are available before executing them. Returns tool slugs, descriptions, required parameters, and connection status.`, inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Natural language search query (e.g., "send email", "create github issue", "list slack channels")', }, toolkit: { type: 'string', description: 'Filter to a specific toolkit (e.g., "gmail", "github", "slack")', }, }, required: ['query'], }, execute: async (input) => { const client = deps.getClient(); if (!client) return 'Composio is not configured. The user needs to set up Composio in settings.'; const results = await client.searchTools(input.query, { toolkits: input.toolkit ? [input.toolkit] : undefined, limit: 15, }); if (results.length === 0) { return `No tools found for "${input.query}". Try a broader search or check if the toolkit is connected.`; } const formatted = results.map(t => `- **${t.slug}** (${t.toolkit}): ${t.description}\n Connected: ${t.connected ? 'Yes' : 'No — user must connect first'}\n Params: ${t.parameters?.map(p => p.name).join(', ') || 'none'}` ).join('\n\n'); return { output: `Found ${results.length} tools:\n\n${formatted}`, metadata: { type: 'bytespace_search', resultCount: results.length, toolkits: [...new Set(results.map(t => t.toolkit))], }, } as ToolResult; }, }; }

Tool 2: bytespace_execute — Run an Action

typescript
// packages/daemon/src/tools/composio-execute.ts import { Tool, ToolResult } from './types'; export function createComposioExecuteTool(deps: { getClient: () => ProxiedComposioClient | null; }): Tool { return { name: 'bytespace_execute', description: `Execute an action on a connected service via Composio. Use bytespace_search first to discover available tools and their parameters. The user must have an active connection to the toolkit for the action to succeed. If the user is not connected, use bytespace_connect to initiate authentication.`, inputSchema: { type: 'object', properties: { slug: { type: 'string', description: 'The tool slug from bytespace_search (e.g., "GMAIL_SEND_EMAIL", "GITHUB_CREATE_ISSUE")', }, arguments: { type: 'object', description: 'Arguments for the tool as key-value pairs', additionalProperties: true, }, connected_account_id: { type: 'string', description: 'Optional: specific connected account ID if user has multiple accounts for this toolkit', }, }, required: ['slug', 'arguments'], }, execute: async (input) => { const client = deps.getClient(); if (!client) return 'Composio is not configured.'; try { const result = await client.executeTool(input.slug, { ...input.arguments, ...(input.connected_account_id && { connected_account_id: input.connected_account_id, }), }); const output = typeof result === 'string' ? result : JSON.stringify(result, null, 2); return { output: `Action ${input.slug} executed successfully:\n\n${output}`, metadata: { type: 'bytespace_execute', slug: input.slug, success: true, }, } as ToolResult; } catch (error) { const message = error instanceof Error ? error.message : String(error); // Detect auth errors and guide agent to connect if (message.includes('401') || message.includes('not connected') || message.includes('no active connection')) { const toolkit = input.slug.split('_')[0].toLowerCase(); return `The user is not connected to ${toolkit}. Use bytespace_connect to initiate authentication before retrying this action.`; } return `Action ${input.slug} failed: ${message}`; } }, }; }

Tool 3: bytespace_connect — Initiate Service Authentication

typescript
// packages/daemon/src/tools/composio-connect.ts import { Tool, ToolResult } from './types'; export function createComposioConnectTool(deps: { getClient: () => ProxiedComposioClient | null; }): Tool { return { name: 'bytespace_connect', description: `Connect the user to an external service (Gmail, GitHub, Slack, etc.) via OAuth or API key. Returns an authentication URL that the user must visit to authorize access. Use this when: - The user explicitly asks to connect a service - A bytespace_execute call fails because the user is not connected - You need to check which services are already connected Actions: "connect" (initiate OAuth), "list" (show connections), "disconnect" (remove connection)`, inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['connect', 'list', 'disconnect'], description: 'The action to perform', }, toolkit: { type: 'string', description: 'The service to connect (e.g., "gmail", "github", "slack", "notion")', }, connected_account_id: { type: 'string', description: 'For disconnect: the connected account ID to remove', }, }, required: ['action'], }, execute: async (input) => { const client = deps.getClient(); if (!client) return 'Composio is not configured.'; switch (input.action) { case 'connect': { if (!input.toolkit) return 'Please specify a toolkit to connect (e.g., "gmail", "github").'; const result = await client.initiateConnection(input.toolkit); return { output: `To connect ${input.toolkit}, the user needs to authenticate:\n\n**Auth URL:** ${result.redirectUrl}\n\nPlease open this URL to authorize bot0 to access your ${input.toolkit} account. The connection will be saved automatically.`, metadata: { type: 'bytespace_connect', action: 'connect', toolkit: input.toolkit, authUrl: result.redirectUrl, connectionId: result.connectionId, }, } as ToolResult; } case 'list': { const connections = await client.listConnections({ toolkit: input.toolkit, status: 'ACTIVE', }); if (connections.length === 0) { return input.toolkit ? `No active connections to ${input.toolkit}. Use bytespace_connect with action "connect" to set one up.` : 'No active connections. The user has not connected any external services yet.'; } const formatted = connections.map(c => `- **${c.toolkit}** (${c.id}): ${c.status} — connected ${c.createdAt}` ).join('\n'); return { output: `Active connections:\n\n${formatted}`, metadata: { type: 'bytespace_connect', action: 'list', connectionCount: connections.length, }, } as ToolResult; } case 'disconnect': { if (!input.connected_account_id) return 'Please specify connected_account_id to disconnect.'; // Composio API: DELETE connected account return `Connection ${input.connected_account_id} has been disconnected.`; } } }, }; }

Tool 4: bytespace_trigger — Manage Event Triggers

typescript
// packages/daemon/src/tools/composio-trigger.ts import { Tool, ToolResult } from './types'; export function createComposioTriggerTool(deps: { getClient: () => ProxiedComposioClient | null; }): Tool { return { name: 'bytespace_trigger', description: `Manage event triggers for connected services. Triggers notify the daemon when something happens (new email, GitHub PR, Slack message, etc.). This is how the agent becomes proactive — reacting to real-world events. Actions: - "create": Set up a new trigger (e.g., "notify me when I get a GitHub PR review") - "list": Show active triggers - "list_types": Show available trigger types for a toolkit - "disable" / "enable" / "delete": Manage existing triggers`, inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['create', 'list', 'list_types', 'enable', 'disable', 'delete'], }, trigger_slug: { type: 'string', description: 'Trigger type slug (e.g., "GMAIL_NEW_GMAIL_MESSAGE", "GITHUB_COMMIT_EVENT")', }, trigger_config: { type: 'object', description: 'Configuration for the trigger (e.g., { "owner": "myorg", "repo": "myrepo" })', additionalProperties: true, }, trigger_id: { type: 'string', description: 'For enable/disable/delete: the trigger instance ID', }, toolkit: { type: 'string', description: 'For list_types: filter by toolkit', }, }, required: ['action'], }, execute: async (input) => { const client = deps.getClient(); if (!client) return 'Composio is not configured.'; switch (input.action) { case 'create': { if (!input.trigger_slug) return 'Specify trigger_slug (e.g., "GMAIL_NEW_GMAIL_MESSAGE").'; const result = await client.createTrigger( input.trigger_slug, input.trigger_config ?? {} ); return { output: `Trigger created: ${result.triggerId}\nType: ${input.trigger_slug}\nThe daemon will now receive events when this trigger fires.`, metadata: { type: 'bytespace_trigger', action: 'create', triggerId: result.triggerId }, } as ToolResult; } case 'list': { const triggers = await client.listTriggers(); if (triggers.length === 0) return 'No active triggers.'; const formatted = triggers.map(t => `- **${t.slug}** (${t.id}): ${t.status}${t.toolkit}` ).join('\n'); return { output: `Active triggers:\n\n${formatted}`, metadata: { type: 'bytespace_trigger', action: 'list', count: triggers.length } } as ToolResult; } case 'list_types': { const types = await client.listTriggerTypes(input.toolkit); if (types.length === 0) return `No trigger types available${input.toolkit ? ` for ${input.toolkit}` : ''}.`; const formatted = types.map(t => `- **${t.slug}**: ${t.description}\n Config: ${JSON.stringify(t.config ?? {})}` ).join('\n\n'); return { output: `Available trigger types:\n\n${formatted}`, metadata: { type: 'bytespace_trigger', action: 'list_types' } } as ToolResult; } case 'enable': case 'disable': case 'delete': { if (!input.trigger_id) return `Specify trigger_id to ${input.action}.`; await client.manageTrigger(input.trigger_id, input.action); return `Trigger ${input.trigger_id} has been ${input.action}d.`; } } }, }; }

Tool Registration

typescript
// In packages/daemon/src/index.ts — createDefaultToolRegistry() import { createComposioSearchTool, createComposioExecuteTool, createComposioConnectTool, createComposioTriggerTool } from './tools'; import { ProxiedComposioClient } from './composio/client'; export function createDefaultToolRegistry(options?: { taskToolDeps?: TaskToolDeps; composioDeps?: { getConfig: () => DaemonConfig; getSigningProvider: () => SigningProvider | null }; }) { const registry = new ToolRegistry(); // ... existing tools ... // Composio tools (lazy client) let composioClient: ProxiedComposioClient | null = null; const getComposioClient = () => { if (!composioClient && options?.composioDeps) { composioClient = new ProxiedComposioClient(options.composioDeps); } return composioClient; }; registry.register(createComposioSearchTool({ getClient: getComposioClient })); registry.register(createComposioExecuteTool({ getClient: getComposioClient })); registry.register(createComposioConnectTool({ getClient: getComposioClient })); registry.register(createComposioTriggerTool({ getClient: getComposioClient })); return registry; }

System Prompt Addition

Add Composio awareness to the agent's system prompt in packages/daemon/src/agent/loop.ts:

typescript
// In buildSystemPrompt(), add after tool usage section: const composioPrompt = ` ## External Service Integration (Composio) You have access to 500+ external services through the bytespace_* tools: - bytespace_search: Find available tools across Gmail, Slack, GitHub, Salesforce, etc. - bytespace_execute: Run actions on connected services - bytespace_connect: Help the user connect new services (OAuth) - bytespace_trigger: Set up event triggers for proactive notifications **Workflow for service actions:** 1. If unsure what tools exist, use bytespace_search first 2. If the user isn't connected, use bytespace_connect to initiate OAuth 3. Execute actions with bytespace_execute 4. Set up triggers with bytespace_trigger for ongoing monitoring **Connection handling:** - If an action fails with "not connected", offer to connect the service - The user authenticates via a URL you provide — never ask for passwords or API keys - Connected services persist across sessions — no need to reconnect **Triggers:** - Triggers make you proactive — you'll be notified of events automatically - Always confirm with the user before creating triggers (they generate ongoing notifications) - Common triggers: new emails, GitHub PRs, Slack messages, calendar events `;

4. Auth & Connection Management

OAuth Flow (User Connects a Service)

Agent detects user needs Gmail access
         │
         │ bytespace_connect(action: "connect", toolkit: "gmail")
         ▼
┌─────────────────┐
│  Daemon Tool     │  Calls proxy → Composio API
│                  │  Gets redirect URL
└────────┬────────┘
         │
         │ Returns auth URL to agent
         ▼
Agent presents URL to user in chat:
"Click here to connect Gmail: https://app.composio.dev/link/abc..."
         │
         │ User clicks link → browser opens
         ▼
┌─────────────────┐
│  Composio Auth   │  White-labeled OAuth screen (bot0 branding)
│  Page            │  → Google OAuth consent
│                  │  → Token exchange
│                  │  → Store encrypted
└────────┬────────┘
         │
         │ Callback to: https://bytespace.bot0.dev/api/auth-apps/callback
         │   ?status=success&connected_account_id=ca_xyz
         ▼
┌─────────────────┐
│  Bytespace       │  1. Parse callback
│  Callback Route  │  2. Notify daemon via Hub/IPC
│                  │  3. Show success page to user
└────────┬────────┘
         │
         │ Event: connection_complete
         ▼
Daemon receives notification:
"Gmail connected for user usr_abc123 (ca_xyz)"
Agent can now execute Gmail tools

Desktop UI Integration for Auth

The Desktop app handles auth URLs with a dedicated flow:

typescript
// When agent returns bytespace_connect metadata with authUrl: // Desktop intercepts and opens in system browser (not in-app) // This is critical for OAuth — Composio's auth page needs a real browser // In Terminal.tsx / MessageItem.tsx: if (metadata?.type === 'bytespace_connect' && metadata.authUrl) { // Render a clickable button instead of raw URL <ComposioAuthButton url={metadata.authUrl} toolkit={metadata.toolkit} onComplete={() => { // Optionally poll connection status }} /> }

5. Trigger System

This is the most important infrastructure piece — triggers make the agent alive.

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                    TRIGGER LIFECYCLE                              │
│                                                                  │
│  1. REGISTRATION                                                 │
│     Agent → bytespace_trigger(create) → Proxy → Composio API     │
│     Composio stores trigger + configures webhook/poll            │
│                                                                  │
│  2. EVENT OCCURS                                                 │
│     Gmail receives email / GitHub gets PR / Slack gets message   │
│                                                                  │
│  3. COMPOSIO CAPTURES                                            │
│     Webhook: Service pushes to Composio immediately              │
│     Polling: Composio polls every 60s, detects change            │
│                                                                  │
│  4. COMPOSIO DELIVERS                                            │
│     POST /api/webhooks/composio on Bytespace                     │
│     Headers: webhook-signature, x-composio-webhook-version       │
│     Body: V3 payload with trigger data + metadata                │
│                                                                  │
│  5. BYTESPACE ROUTES                                             │
│     Verify signature → Parse payload → Resolve user/daemon       │
│     → Hub dispatch (online) or queue (offline)                   │
│                                                                  │
│  6. DAEMON PROCESSES                                             │
│     Proactive engine evaluates significance                      │
│     Permission check → Execute or ask user                       │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Webhook Handler

typescript
// apps/bytespace/src/app/api/webhooks/composio/route.ts import { NextRequest, NextResponse } from 'next/server'; import crypto from 'crypto'; const COMPOSIO_WEBHOOK_SECRET = process.env.COMPOSIO_WEBHOOK_SECRET!; export async function POST(request: NextRequest) { const bodyText = await request.text(); const signature = request.headers.get('webhook-signature'); const webhookVersion = request.headers.get('x-composio-webhook-version'); // 1. Verify HMAC-SHA256 signature if (!verifyWebhookSignature(bodyText, signature)) { return NextResponse.json({ error: 'Invalid signature' }, { status: 401 }); } // 2. Parse V3 payload const payload = JSON.parse(bodyText); const { log_id, timestamp, type: eventType, // e.g., "gmail_new_gmail_message" data: { user_id: composioUserId, // = bot0 userId trigger_id, trigger_nano_id, connection_id, ...triggerData }, } = payload; // 3. Resolve user → active daemon const daemon = await findActiveDaemonForUser(composioUserId); // 4. Create trigger event record const event = await db.insert(ctx0TriggerEvents).values({ id: generateId(), user_id: composioUserId, trigger_id: trigger_nano_id, event_type: eventType, payload: triggerData, status: daemon ? 'dispatched' : 'pending', created_at: new Date(timestamp), }); // 5. Route to daemon if (daemon) { // Daemon is online — dispatch via Hub await dispatchViaHub(daemon.device_id, { type: 'trigger_event', eventId: event.id, triggerType: eventType, triggerId: trigger_nano_id, payload: triggerData, timestamp, }); } // If offline, event stays 'pending' — daemon polls on reconnect return NextResponse.json({ received: true, log_id }); } function verifyWebhookSignature(body: string, signatureHeader: string | null): boolean { if (!signatureHeader) return false; // Format: "v1,<base64_signature>" const [version, signature] = signatureHeader.split(','); if (version !== 'v1') return false; const expected = crypto .createHmac('sha256', COMPOSIO_WEBHOOK_SECRET) .update(body) .digest('base64'); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) ); }

Daemon-Side Trigger Processing

When a trigger event arrives at the daemon (via Hub WebSocket), it enters the proactive engine:

typescript
// packages/daemon/src/composio/trigger-handler.ts import { Bus } from '../events/bus'; import { TriggerEvent } from '../events/events'; export interface TriggerEventPayload { eventId: string; triggerType: string; // e.g., "gmail_new_gmail_message" triggerId: string; // Composio trigger nano ID payload: unknown; // Service-specific event data timestamp: string; } export class TriggerHandler { constructor( private bus: Bus, private getAgent: () => Bot0Agent, ) {} /** * Called when a trigger event arrives from the Hub. * This is the bridge between Composio triggers and the proactive engine. */ async handleTriggerEvent(event: TriggerEventPayload): Promise<void> { // 1. Publish to event bus (for UI notification) this.bus.publish(TriggerEvent, { eventId: event.eventId, triggerType: event.triggerType, triggerId: event.triggerId, timestamp: event.timestamp, }); // 2. Format trigger as a proactive task prompt const prompt = this.formatTriggerPrompt(event); // 3. Submit to proactive engine / task queue // The proactive engine evaluates significance, checks permissions, // and either executes autonomously or asks the user await this.submitToProactiveEngine({ source: 'trigger', sourceId: event.triggerId, eventId: event.eventId, prompt, priority: this.inferPriority(event.triggerType), payload: event.payload, }); } private formatTriggerPrompt(event: TriggerEventPayload): string { // Create a natural language prompt from the trigger event // This is what the agent "sees" when processing the trigger const payloadSummary = JSON.stringify(event.payload, null, 2); return `[TRIGGER EVENT: ${event.triggerType}] A trigger event fired. Here are the details: Trigger type: ${event.triggerType} Trigger ID: ${event.triggerId} Timestamp: ${event.timestamp} Event data: ${payloadSummary} Based on this event, determine the appropriate action. Consider: 1. Is this significant enough to notify the user? 2. Can you take autonomous action based on existing permissions? 3. Should you ask the user what to do?`; } private inferPriority(triggerType: string): 'critical' | 'high' | 'normal' | 'low' { // Payment/billing triggers are critical if (triggerType.includes('payment') || triggerType.includes('billing')) return 'critical'; // Direct messages are high priority if (triggerType.includes('message') || triggerType.includes('mention')) return 'high'; // Most triggers are normal return 'normal'; } }

Trigger Event Persistence & Offline Handling

When a daemon is offline, trigger events are queued in the database:

sql
-- New table: ctx0_trigger_events CREATE TABLE ctx0_trigger_events ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES ctx0_users(id), trigger_id TEXT NOT NULL, -- Composio trigger nano ID event_type TEXT NOT NULL, -- e.g., "gmail_new_gmail_message" payload JSONB NOT NULL DEFAULT '{}', status TEXT NOT NULL DEFAULT 'pending', -- pending | dispatched | processed | failed dispatched_at TIMESTAMPTZ, processed_at TIMESTAMPTZ, error TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), ttl_expires_at TIMESTAMPTZ -- Auto-expire old events ); CREATE INDEX idx_trigger_events_pending ON ctx0_trigger_events(user_id, status) WHERE status = 'pending';

When the daemon reconnects, it polls for pending events:

typescript
// On daemon reconnect / startup: async function processPendingTriggerEvents( composioClient: ProxiedComposioClient, triggerHandler: TriggerHandler, userId: string ): Promise<void> { const pending = await db .select() .from(ctx0TriggerEvents) .where( and( eq(ctx0TriggerEvents.user_id, userId), eq(ctx0TriggerEvents.status, 'pending'), // Only process events less than 24h old gt(ctx0TriggerEvents.created_at, sql`NOW() - INTERVAL '24 hours'`) ) ) .orderBy(ctx0TriggerEvents.created_at); for (const event of pending) { await triggerHandler.handleTriggerEvent({ eventId: event.id, triggerType: event.event_type, triggerId: event.trigger_id, payload: event.payload, timestamp: event.created_at.toISOString(), }); // Mark as dispatched await db.update(ctx0TriggerEvents) .set({ status: 'dispatched', dispatched_at: new Date() }) .where(eq(ctx0TriggerEvents.id, event.id)); } }

6. Desktop UI Integration

Connection Status in Settings

The Settings panel gains a new "Connections" tab showing Composio-managed service connections:

typescript
// packages/desktop/src/components/ConnectionsPanel.tsx // Lists active connections with: // - Service icon + name // - Connection status (active/expired) // - Connected at timestamp // - Disconnect button // - "Connect new service" button → opens toolkit browser // The agent can also trigger connections via bytespace_connect, // which shows an inline auth button in the chat.

Trigger Management UI

typescript
// packages/desktop/src/components/TriggersPanel.tsx // Lists active triggers with: // - Trigger type + description // - Service icon // - Status (active/paused) // - Last fired timestamp // - Enable/disable toggle // - Delete button // - "Create trigger" button → opens trigger browser

Auth URL Handling

When the agent returns a bytespace_connect metadata with authUrl, the Desktop renders a clickable button:

typescript
// In MessageItem.tsx, when rendering bytespace_connect tool results: <div className="composio-auth-prompt"> <p>To connect {toolkit}, click the button below:</p> <button onClick={() => shell.openExternal(authUrl)}> Connect {toolkit} </button> <p className="text-muted"> This will open your browser for secure authentication. </p> </div>

Security Model

Principle: Composio API Key Never Touches Daemon

┌─────────────────────────────────────┐
│           DAEMON                     │
│                                      │
│  Has: session token, proxy URL       │
│  Does NOT have: Composio API key     │
│                                      │
│  All Composio calls → Proxy          │
│  Device signature on every request   │
└──────────────────┬──────────────────┘
                   │
                   │ HTTPS + device sig
                   ▼
┌─────────────────────────────────────┐
│           PROXY (Bytespace)          │
│                                      │
│  Has: Composio API key (encrypted)   │
│  Validates: session + device sig     │
│  Forwards: request to Composio       │
│  Injects: x-api-key header           │
└──────────────────┬──────────────────┘
                   │
                   ▼
┌─────────────────────────────────────┐
│           COMPOSIO API               │
│                                      │
│  Validates: API key                  │
│  Scopes: user_id isolation           │
│  Manages: OAuth tokens, credentials  │
└─────────────────────────────────────┘

Attack Scenarios & Mitigations

AttackMitigation
Prompt injection tries to exfiltrate OAuth tokensTokens stored by Composio, never in daemon context. Agent only has tool slugs.
Compromised daemon tries to access other users' connectionsProxy maps session token → userId. Composio scopes all operations to userId.
Stolen session token used from another machineDevice signature validation — token is useless without hardware key.
Webhook replay attackHMAC-SHA256 signature verification + timestamp check + idempotency via log_id.
Agent creates unauthorized triggersPermission middleware gates bytespace_trigger — user must approve trigger creation.
Malicious trigger payload injectionWebhook signature verification prevents forged payloads. Composio signs all deliveries.

Permission Categories for Composio Tools

typescript
// In packages/daemon/src/permissions/middleware.ts function getToolPermission(toolName: string, input: any): PermissionRequirement | null { switch (toolName) { case 'bytespace_search': // Search is read-only — auto-approve in auto mode return null; case 'bytespace_execute': // Executing actions requires permission return { permission: 'composio:execute', patterns: [input.slug], metadata: { slug: input.slug, toolkit: input.slug?.split('_')[0] }, }; case 'bytespace_connect': if (input.action === 'list') return null; // Read-only return { permission: 'composio:connect', patterns: [input.toolkit], metadata: { toolkit: input.toolkit }, }; case 'bytespace_trigger': if (input.action === 'list' || input.action === 'list_types') return null; return { permission: 'composio:trigger', patterns: [input.trigger_slug ?? input.trigger_id ?? '*'], metadata: { action: input.action }, }; } }

Permission Escalation Rules

ToolAuto ModeAsk Mode
bytespace_searchAuto-approveAuto-approve
bytespace_execute (read actions)Auto-approveAsk first time, then rule
bytespace_execute (write actions)Ask first timeAlways ask
bytespace_execute (delete actions)Always askAlways ask
bytespace_connect (list)Auto-approveAuto-approve
bytespace_connect (connect/disconnect)Always askAlways ask
bytespace_trigger (list/list_types)Auto-approveAuto-approve
bytespace_trigger (create)Always askAlways ask
bytespace_trigger (enable/disable)Ask first timeAlways ask
bytespace_trigger (delete)Always askAlways ask

The "Always ask" for connection and trigger creation is intentional — these are high-impact operations that grant ongoing access or create persistent event subscriptions.


Data Model

New Tables

sql
-- Track Composio connections locally for offline reference -- This is a CACHE of Composio's state, not the source of truth CREATE TABLE ctx0_bytespace_connections ( id TEXT PRIMARY KEY, -- Composio connected_account nano ID (ca_xyz) user_id TEXT NOT NULL REFERENCES ctx0_users(id), toolkit TEXT NOT NULL, -- e.g., "gmail", "github" status TEXT NOT NULL DEFAULT 'active', -- active | expired | initiated display_name TEXT, -- e.g., "Work Gmail ([email protected])" auth_scheme TEXT, -- OAUTH2 | API_KEY | BASIC connected_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), expires_at TIMESTAMPTZ, synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -- Last sync with Composio ); -- Trigger event queue (for offline delivery) CREATE TABLE ctx0_trigger_events ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES ctx0_users(id), trigger_id TEXT NOT NULL, event_type TEXT NOT NULL, payload JSONB NOT NULL DEFAULT '{}', status TEXT NOT NULL DEFAULT 'pending', dispatched_at TIMESTAMPTZ, processed_at TIMESTAMPTZ, error TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), ttl_expires_at TIMESTAMPTZ ); -- Trigger subscriptions (local cache for UI display) CREATE TABLE ctx0_bytespace_triggers ( id TEXT PRIMARY KEY, -- Composio trigger nano ID user_id TEXT NOT NULL REFERENCES ctx0_users(id), slug TEXT NOT NULL, -- e.g., "GMAIL_NEW_GMAIL_MESSAGE" toolkit TEXT NOT NULL, config JSONB NOT NULL DEFAULT '{}', -- Trigger configuration status TEXT NOT NULL DEFAULT 'active', description TEXT, -- Human-readable description created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), last_fired_at TIMESTAMPTZ, fire_count INTEGER NOT NULL DEFAULT 0, synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW() );

Schema Location

Add these to packages/ctx0/src/schema/:

FileTable
composio-connections.tsctx0_bytespace_connections
composio-triggers.tsctx0_bytespace_triggers
trigger-events.tsctx0_trigger_events

Register in ctx0Tables array in packages/ctx0/src/schema/index.ts.


White-Label Authentication

Goal

Users never see "Composio" anywhere. The auth experience is fully bot0-branded.

Three Layers of White-Labeling

Layer 1: Auth Screen Customization (Composio Dashboard)

In Composio Project Settings → Auth Screen:

  • Upload bot0 logo
  • Set app title to "bot0"
  • This applies to all Connect Link flows

Layer 2: Custom OAuth Apps (Per Toolkit)

For major toolkits (Google, GitHub, Slack, Microsoft), register bot0's own OAuth apps:

typescript
// Create custom auth config for Gmail with bot0's OAuth credentials const gmailAuthConfig = await composio.authConfigs.create('GMAIL', { name: 'bot0 Gmail', type: 'use_custom_auth', credentials: { client_id: process.env.GOOGLE_OAUTH_CLIENT_ID, client_secret: process.env.GOOGLE_OAUTH_CLIENT_SECRET, oauth_redirect_uri: 'https://bytespace.bot0.dev/api/auth-apps/callback', }, authScheme: 'OAUTH2', });

This means when a user authorizes Gmail, the Google consent screen shows "bot0" (not "Composio").

Layer 3: Custom Redirect Domain

Route the OAuth callback through bot0's domain:

typescript
// apps/bytespace/src/app/api/auth-apps/callback/route.ts // // This is a passthrough that: // 1. Receives the OAuth callback on bytespace.bot0.dev // 2. Forwards to Composio's actual callback handler // 3. Redirects user back to bot0 Desktop app export async function GET(request: NextRequest) { const params = request.nextUrl.searchParams; // Forward to Composio's callback const composioCallback = new URL('https://backend.composio.dev/api/v3/toolkits/auth/callback'); params.forEach((value, key) => composioCallback.searchParams.set(key, value)); const response = await fetch(composioCallback.toString()); // Parse result const status = params.get('status'); const connectedAccountId = params.get('connected_account_id'); // Redirect back to bot0 Desktop (deep link or success page) const redirectUrl = new URL('https://bytespace.bot0.dev/connections/success'); if (status) redirectUrl.searchParams.set('status', status); if (connectedAccountId) redirectUrl.searchParams.set('connected_account_id', connectedAccountId); return NextResponse.redirect(redirectUrl); }

Custom OAuth Apps to Register

ServicePriorityOAuth App Location
Google (Gmail, Drive, Calendar)P0Google Cloud Console
GitHubP0GitHub Developer Settings
SlackP0Slack API
Microsoft (Outlook, OneDrive)P1Azure AD
NotionP1Notion Integrations
LinearP2Linear API Settings
SalesforceP2Salesforce Connected Apps

For other toolkits, use Composio's managed auth (users see Composio's default OAuth app, which is fine for less common integrations).


Trigger Architecture (Deep Dive)

Why Triggers Are Critical

The current bot0 proactive engine has three input sources:

  1. Watchers — Polling local filesystem, clipboard, etc.
  2. Schedules — Cron-based time triggers
  3. Ingestors — Webhook receivers

Composio triggers supercharge source #3. Instead of building webhook receivers for each service, bot0 gets:

  • Gmail → new email trigger (polling, ~60s delay)
  • GitHub → PR, issue, commit triggers (real-time webhook)
  • Slack → message, mention triggers (real-time webhook)
  • Google Calendar → event triggers
  • Stripe → payment, subscription triggers
  • HubSpot → deal, contact triggers
  • And hundreds more

Trigger Registration Flow

User: "Alert me when someone stars my GitHub repo composio/bot0"
         │
         ▼
Agent reasons:
  1. Need GITHUB_STAR_EVENT trigger
  2. User must be connected to GitHub
  3. Config needs: owner=composio, repo=bot0
         │
         │ bytespace_connect(action: "list", toolkit: "github")
         ▼
Agent sees: GitHub connected ✓
         │
         │ bytespace_trigger(action: "list_types", toolkit: "github")
         ▼
Agent discovers GITHUB_STAR_EVENT trigger type
         │
         │ bytespace_trigger(action: "create",
         │   trigger_slug: "GITHUB_STAR_EVENT",
         │   trigger_config: { owner: "composio", repo: "bot0" })
         ▼
Permission prompt: "Create trigger GITHUB_STAR_EVENT for github?"
User approves ✓
         │
         ▼
Trigger created → Composio configures GitHub webhook
         │
         ▼
[Later: Someone stars the repo]
         │
         ▼
GitHub → Composio → Bytespace webhook → Hub → Daemon
         │
         ▼
Proactive engine evaluates:
  "New star on composio/bot0 by user xyz"
  Priority: low
  Action: Notify user (non-urgent)
         │
         ▼
Agent: "Your repo composio/bot0 just got a new star from @xyz! 🌟"

Trigger-to-Proactive Engine Bridge

The proactive engine (documented in bot0-proactive-architecture.md) has a well-defined pipeline:

Collect → Batch → Evaluate → Permission → Execute/Ask/Store → Learn

Composio triggers feed into the Collect stage:

typescript
// packages/daemon/src/proactive/ingestors/composio.ts export class ComposioTriggerIngestor implements TriggerIngestor { readonly source = 'composio'; readonly batchWindow = 30_000; // 30 seconds readonly maxBatchSize = 50; /** * Transform a raw Composio trigger event into an observation * for the proactive engine's batch queue. */ ingest(event: TriggerEventPayload): Observation { return { id: event.eventId, source: 'composio', sourceId: event.triggerId, type: event.triggerType, data: event.payload, timestamp: new Date(event.timestamp), priority: this.inferPriority(event.triggerType), }; } /** * Batching behavior: * - GitHub star events: collapse to count ("5 new stars") * - Email triggers: collapse by sender * - Slack messages: collapse by channel * - Payment events: never batch (always critical) */ batch(observations: Observation[]): BatchedObservation[] { // Group by trigger type + context const groups = groupBy(observations, o => `${o.type}:${o.sourceId}`); return Object.entries(groups).map(([key, obs]) => ({ id: generateId(), source: 'composio', count: obs.length, triggerType: obs[0].type, firstAt: obs[0].timestamp, lastAt: obs[obs.length - 1].timestamp, summary: this.summarizeBatch(obs), observations: obs, })); } }

Trigger Categories & Behavior

CategoryExamplesDeliveryBatchPriority
Real-time (webhook)GitHub PR, Slack message, Stripe paymentInstant30s windowHigh
PollingGmail new email, Google Drive changes~60s delay30s windowNormal
ThresholdCustom — revenue drop, churn rateVia Composio computed triggersNo batchCritical

Trigger Persistence & Survival

Triggers survive daemon restarts because they're registered in Composio (cloud-side). The daemon doesn't need to re-register triggers on startup — it only needs to:

  1. Ensure the webhook URL is still configured (Bytespace endpoint)
  2. Poll for any events that arrived while offline
  3. Sync trigger list to local cache (ctx0_bytespace_triggers)

Multi-Account Management

Problem

A user might have:

  • Work Gmail + Personal Gmail
  • Multiple GitHub accounts
  • Work Slack + community Slack

Solution: Connected Account IDs

Composio already supports multiple connected accounts per toolkit per user. Each connection gets a unique connected_account_id (e.g., ca_abc123).

typescript
// When user has multiple Gmail accounts: const connections = await client.listConnections({ toolkit: 'gmail' }); // Returns: // [ // { id: 'ca_work123', toolkit: 'gmail', displayName: 'Work ([email protected])' }, // { id: 'ca_personal456', toolkit: 'gmail', displayName: 'Personal ([email protected])' }, // ] // Agent can specify which account to use: await client.executeTool('GMAIL_SEND_EMAIL', { connected_account_id: 'ca_work123', // Use work email to: '[email protected]', subject: 'Meeting follow-up', body: '...', });

Agent Behavior with Multiple Accounts

The agent learns which account to use via context:

System prompt addition:

"When the user has multiple accounts for a service, ask which one to use
if the context is ambiguous. If the context is clear (e.g., 'send from
my work email'), use the appropriate account. You can list accounts with
bytespace_connect(action: 'list', toolkit: '<toolkit>')."

Account Disambiguation in Triggers

When creating triggers, the user must specify which account:

User: "Notify me when I get important emails"

Agent: "You have two Gmail accounts connected:
1. Work ([email protected])
2. Personal ([email protected])

Which account should I monitor for important emails? Or both?"

User: "Both"

Agent: Creates two trigger instances, one per connected account

Permission System

Composio-Specific Permission Rules

The existing permission engine (auto/ask modes with custom rules) extends naturally to Composio tools:

typescript
// Permission rule examples: // "Always allow searching for tools" { toolName: 'bytespace_search', permission: 'composio:search', mode: 'auto' } // "Always allow reading Gmail (but ask for sending)" { toolName: 'bytespace_execute', permission: 'composio:execute', patterns: ['GMAIL_LIST_*', 'GMAIL_GET_*'], mode: 'auto' } // "Ask before any write operations" { toolName: 'bytespace_execute', permission: 'composio:execute', patterns: ['*_CREATE_*', '*_SEND_*', '*_DELETE_*', '*_UPDATE_*'], mode: 'ask' } // "Always ask before creating triggers" { toolName: 'bytespace_trigger', permission: 'composio:trigger', patterns: ['create'], mode: 'ask' }

Permission Learning (from Proactive Architecture)

The proactive engine's permission learning system applies to Composio actions:

1st time: Agent wants to send Slack message → Ask user
2nd time: Agent wants to send Slack message → Ask user
3rd time: Agent wants to send Slack message → Ask user
4th time: Agent wants to send Slack message → Ask user
5th time: Agent wants to send Slack message → Ask user
→ 5 consecutive approvals → confidence = 95% → Auto-approve future Slack sends

This is configured per user, per action pattern, and persists across sessions.


Implementation Plan

Phase 1: Foundation (Week 1-2)

Goal: Composio tools work in the agent loop, users can connect services and execute actions.

TaskFileDescription
1.1packages/daemon/src/composio/client.tsProxiedComposioClient
1.2packages/daemon/src/composio/types.tsTypeScript types for Composio responses
1.3apps/bytespace/src/app/api/proxy/composio/[...path]/route.tsProxy endpoint
1.4packages/daemon/src/tools/composio-search.tsSearch tool
1.5packages/daemon/src/tools/composio-execute.tsExecute tool
1.6packages/daemon/src/tools/composio-connect.tsConnect tool
1.7packages/daemon/src/tools/index.tsExport new tools
1.8packages/daemon/src/index.tsRegister tools in default registry
1.9packages/daemon/src/permissions/middleware.tsPermission rules for Composio tools
1.10packages/daemon/src/agent/loop.tsSystem prompt addition

Dependencies:

  • @composio/core added to proxy's package.json (for webhook verification only)
  • Composio API key stored in Bytespace encrypted storage
  • Composio project created with webhook URL configured

Phase 2: White-Label Auth (Week 2-3)

Goal: Users see bot0 branding throughout the OAuth flow.

TaskDescription
2.1Configure Composio dashboard auth screen (logo, app name)
2.2Register Google OAuth app under bot0's GCP project
2.3Register GitHub OAuth app under bot0's org
2.4Register Slack OAuth app
2.5Create custom auth configs in Composio for each OAuth app
2.6apps/bytespace/src/app/api/auth-apps/callback/route.ts — Custom callback handler
2.7Test full OAuth flow: agent → auth URL → consent → callback → connected

Phase 3: Triggers (Week 3-4)

Goal: Trigger events flow from external services to the daemon's proactive engine.

TaskFileDescription
3.1apps/bytespace/src/app/api/webhooks/composio/route.tsWebhook receiver
3.2packages/ctx0/src/schema/trigger-events.tsEvent queue table
3.3packages/ctx0/src/schema/composio-triggers.tsTrigger cache table
3.4packages/daemon/src/tools/composio-trigger.tsTrigger management tool
3.5packages/daemon/src/composio/trigger-handler.tsDaemon-side event handler
3.6Hub protocol: trigger_event message typeRoute triggers via Hub
3.7Daemon startup: poll pending trigger eventsOffline recovery
3.8Proactive engine: ComposioTriggerIngestorBridge to proactive pipeline

Phase 4: Desktop UI (Week 4-5)

Goal: Users can manage connections and triggers from the Desktop app.

TaskDescription
4.1ConnectionsPanel.tsx — List/manage connected services
4.2TriggersPanel.tsx — List/manage active triggers
4.3Auth URL button rendering in chat messages
4.4Trigger event notifications in the UI
4.5Settings panel integration (Composio section)

Phase 5: Multi-Account & Polish (Week 5-6)

Goal: Multiple accounts per service, account disambiguation, connection health monitoring.

TaskDescription
5.1Multi-account support in bytespace_connect and bytespace_execute
5.2Account selector UI in Desktop
5.3Connection health monitoring (detect expired connections)
5.4Trigger event batching in proactive engine
5.5Permission learning for Composio actions

Open Questions

Architecture Decisions

  1. Session model: Tool Router vs Direct Tools?

    • Tool Router (sessions) provides meta-tools that auto-discover and execute — more autonomous but less predictable
    • Direct tools (our approach above) give fine-grained control — we define the tool interface, Composio is just the backend
    • Current decision: Direct tools via proxy. We control the UX.
  2. MCP as alternative integration path?

    • Composio offers MCP server endpoints that could be consumed by the daemon
    • Pro: Standardized protocol, automatic tool discovery
    • Con: Less control over tool presentation, additional hop, harder to customize permissions
    • Current decision: Direct API via proxy. MCP is a future option for advanced users.
  3. Composio API key: one per Bytespace instance or one per user?

    • One per instance: Simpler, all users share one Composio project, isolation via user_id
    • One per user: Users manage their own Composio accounts — more complex but fully self-hosted compatible
    • Current decision: One per Bytespace instance. Self-hosted users can bring their own Composio API key.
  4. Trigger webhook: single endpoint or per-user?

    • Single: One webhook URL for all users, route by user_id in payload
    • Per-user: Individual webhook URLs (Composio MCP pattern)
    • Current decision: Single endpoint. Simpler, and Composio V3 includes user_id in payload.

Pricing & Limits

  1. Which Composio plan for bot0?

    • Hobby (free) is fine for dev/testing
    • Growth ($199/mo) for production with < 5,000 users
    • Enterprise for scale
  2. Rate limiting strategy?

    • Composio: 20K-100K requests per 10 minutes (plan-dependent)
    • bot0 proxy should implement its own rate limiting per user to prevent one user from exhausting the shared quota
    • Proactive engine already has backpressure signals that pause triggers when queue is full

Security

  1. Should self-hosted users bring their own Composio API key?

    • Yes — for fully self-hosted deployments, the user registers their own Composio project
    • The proxy pattern works identically; they just store their own encrypted key
    • Their Composio project → their billing → their data sovereignty
  2. Trigger payload sanitization?

    • Composio trigger payloads are structured JSON, but could contain user-generated content (email bodies, message text)
    • The proactive engine's evaluator should be aware of potential prompt injection in trigger data
    • Mitigation: separate trigger data from agent instructions in the prompt template

Appendix: Composio Terminology Reference

Composio Termbot0 EquivalentDescription
User IDbot0 userIdUnique identifier, 1:1 mapping
ToolkitServicee.g., "gmail", "github", "slack"
ToolActione.g., "GMAIL_SEND_EMAIL"
Auth ConfigOAuth App ConfigDeveloper credentials + scopes
Connected AccountConnectionUser's authenticated link to a service
Trigger InstanceTriggerActive event subscription
Trigger TypeTrigger TemplateDefinition of what events to watch
Session(not used directly)We use direct tools via proxy
Provider(not used directly)We use our own tool wrappers

References