Fix: GitHub composeio (#543)

* feat(composio): add GitHub repository management and trigger creation

- Enhanced the ComposioConnectModal component to support loading and selecting GitHub repositories for connected users.
- Introduced new API functions for listing GitHub repositories and creating triggers, improving integration capabilities with GitHub.
- Added state management for repository loading, selection, and trigger action feedback, enhancing user experience during GitHub integration.
- Updated types to include GitHub repository structures and responses, ensuring type safety and clarity in the codebase.

* feat(composio): add GitHub repository listing and trigger creation APIs

- Implemented new API endpoints for listing GitHub repositories and creating triggers, enhancing integration capabilities with GitHub.
- Updated types to include responses for GitHub repositories and trigger creation, ensuring type safety and clarity.
- Added corresponding handler functions and schemas to support the new operations, improving overall functionality and user experience in the Composio integration.

* refactor(composio): remove GitHub repository and trigger management from ComposioConnectModal

- Eliminated GitHub repository loading and trigger creation logic from the ComposioConnectModal component, streamlining its functionality.
- Removed associated state management and API calls for GitHub repositories and triggers, enhancing code clarity and maintainability.
- Updated types to reflect the removal of GitHub-related structures, ensuring consistency across the codebase.

* format: ran format
This commit is contained in:
YellowSnnowmann
2026-04-14 15:58:12 +05:30
committed by GitHub
parent 21e4a8b307
commit 219a2ff9af
5 changed files with 236 additions and 5 deletions
+42 -2
View File
@@ -17,8 +17,9 @@ use serde_json::json;
use crate::openhuman::integrations::IntegrationClient;
use super::types::{
ComposioAuthorizeResponse, ComposioConnectionsResponse, ComposioDeleteResponse,
ComposioExecuteResponse, ComposioToolkitsResponse, ComposioToolsResponse,
ComposioAuthorizeResponse, ComposioConnectionsResponse, ComposioCreateTriggerResponse,
ComposioDeleteResponse, ComposioExecuteResponse, ComposioGithubReposResponse,
ComposioToolkitsResponse, ComposioToolsResponse,
};
/// High-level client for all backend-proxied Composio operations.
@@ -137,6 +138,45 @@ impl ComposioClient {
.await
}
/// `GET /agent-integrations/composio/github/repos` — list repositories
/// available via the user's authorized GitHub connected account.
pub async fn list_github_repos(
&self,
connection_id: Option<&str>,
) -> Result<ComposioGithubReposResponse> {
let path = match connection_id.map(str::trim).filter(|id| !id.is_empty()) {
Some(id) => format!("/agent-integrations/composio/github/repos?connectionId={id}"),
None => "/agent-integrations/composio/github/repos".to_string(),
};
tracing::debug!(path = %path, "[composio] list_github_repos");
self.inner.get::<ComposioGithubReposResponse>(&path).await
}
/// `POST /agent-integrations/composio/triggers` — create a trigger
/// instance for the authenticated user.
pub async fn create_trigger(
&self,
slug: &str,
connection_id: Option<&str>,
trigger_config: Option<serde_json::Value>,
) -> Result<ComposioCreateTriggerResponse> {
let slug = slug.trim();
if slug.is_empty() {
anyhow::bail!("composio.create_trigger: slug must not be empty");
}
let mut body = json!({ "slug": slug });
if let Some(connection_id) = connection_id.map(str::trim).filter(|id| !id.is_empty()) {
body["connectionId"] = json!(connection_id);
}
if let Some(config) = trigger_config {
body["triggerConfig"] = config;
}
tracing::debug!(slug = %slug, "[composio] create_trigger");
self.inner
.post::<ComposioCreateTriggerResponse>("/agent-integrations/composio/triggers", &body)
.await
}
// ── Raw DELETE ──────────────────────────────────────────────────
/// Perform an HTTP DELETE and parse the standard backend envelope.
+46 -3
View File
@@ -26,9 +26,9 @@ use super::providers::{
get_provider, ProviderContext, ProviderUserProfile, SyncOutcome, SyncReason,
};
use super::types::{
ComposioAuthorizeResponse, ComposioConnectionsResponse, ComposioDeleteResponse,
ComposioExecuteResponse, ComposioToolkitsResponse, ComposioToolsResponse,
ComposioTriggerHistoryResult,
ComposioAuthorizeResponse, ComposioConnectionsResponse, ComposioCreateTriggerResponse,
ComposioDeleteResponse, ComposioExecuteResponse, ComposioGithubReposResponse,
ComposioToolkitsResponse, ComposioToolsResponse, ComposioTriggerHistoryResult,
};
/// Resolve a [`ComposioClient`] from the root config, or return an
@@ -197,6 +197,49 @@ pub async fn composio_execute(
}
}
// ── GitHub repos + trigger provisioning ─────────────────────────────
pub async fn composio_list_github_repos(
config: &Config,
connection_id: Option<String>,
) -> OpResult<RpcOutcome<ComposioGithubReposResponse>> {
tracing::debug!(?connection_id, "[composio] rpc list_github_repos");
let client = resolve_client(config)?;
let resp = client
.list_github_repos(connection_id.as_deref())
.await
.map_err(|e| format!("[composio] list_github_repos failed: {e:#}"))?;
let count = resp.repositories.len();
let connection_id = resp.connection_id.clone();
Ok(RpcOutcome::new(
resp,
vec![format!(
"composio: {count} github repo(s) listed for connection {connection_id}"
)],
))
}
pub async fn composio_create_trigger(
config: &Config,
slug: &str,
connection_id: Option<String>,
trigger_config: Option<serde_json::Value>,
) -> OpResult<RpcOutcome<ComposioCreateTriggerResponse>> {
tracing::debug!(slug = %slug, ?connection_id, "[composio] rpc create_trigger");
let client = resolve_client(config)?;
let resp = client
.create_trigger(slug, connection_id.as_deref(), trigger_config)
.await
.map_err(|e| format!("[composio] create_trigger failed: {e:#}"))?;
let trigger_id = resp.trigger_id.clone();
Ok(RpcOutcome::new(
resp,
vec![format!(
"composio: trigger {trigger_id} created for slug {slug}"
)],
))
}
// ── Trigger history ────────────────────────────────────────────────
pub async fn composio_list_trigger_history(
+106
View File
@@ -8,6 +8,8 @@
//! - `composio.delete_connection` → `openhuman.composio_delete_connection`
//! - `composio.list_tools` → `openhuman.composio_list_tools`
//! - `composio.execute` → `openhuman.composio_execute`
//! - `composio.list_github_repos` → `openhuman.composio_list_github_repos`
//! - `composio.create_trigger` → `openhuman.composio_create_trigger`
//! - `composio.get_user_profile` → `openhuman.composio_get_user_profile`
//! - `composio.sync` → `openhuman.composio_sync`
@@ -24,6 +26,18 @@ struct TriggerHistoryParams {
limit: Option<usize>,
}
#[derive(Debug, serde::Deserialize)]
struct ListGithubReposParams {
connection_id: Option<String>,
}
#[derive(Debug, serde::Deserialize)]
struct CreateTriggerParams {
slug: String,
connection_id: Option<String>,
trigger_config: Option<Value>,
}
pub fn all_controller_schemas() -> Vec<ControllerSchema> {
vec![
schemas("list_toolkits"),
@@ -32,6 +46,8 @@ pub fn all_controller_schemas() -> Vec<ControllerSchema> {
schemas("delete_connection"),
schemas("list_tools"),
schemas("execute"),
schemas("list_github_repos"),
schemas("create_trigger"),
schemas("get_user_profile"),
schemas("sync"),
schemas("list_trigger_history"),
@@ -64,6 +80,14 @@ pub fn all_registered_controllers() -> Vec<RegisteredController> {
schema: schemas("execute"),
handler: handle_execute,
},
RegisteredController {
schema: schemas("list_github_repos"),
handler: handle_list_github_repos,
},
RegisteredController {
schema: schemas("create_trigger"),
handler: handle_create_trigger,
},
RegisteredController {
schema: schemas("get_user_profile"),
handler: handle_get_user_profile,
@@ -191,6 +215,58 @@ pub fn schemas(function: &str) -> ControllerSchema {
required: true,
}],
},
"list_github_repos" => ControllerSchema {
namespace: "composio",
function: "list_github_repos",
description:
"List repositories available through the caller's authorized GitHub Composio connection.",
inputs: vec![FieldSchema {
name: "connection_id",
ty: TypeSchema::Option(Box::new(TypeSchema::String)),
comment:
"Optional GitHub connection id. If omitted, backend picks the first active GitHub connection.",
required: false,
}],
outputs: vec![FieldSchema {
name: "result",
ty: TypeSchema::Json,
comment: "Payload: { connectionId, repositories:[{ owner, repo, fullName, ... }] }.",
required: true,
}],
},
"create_trigger" => ControllerSchema {
namespace: "composio",
function: "create_trigger",
description:
"Create a Composio trigger instance for a connected account. For GitHub triggers, pass owner/repo in trigger_config.",
inputs: vec![
FieldSchema {
name: "slug",
ty: TypeSchema::String,
comment: "Trigger slug, e.g. GITHUB_PULL_REQUEST_EVENT.",
required: true,
},
FieldSchema {
name: "connection_id",
ty: TypeSchema::Option(Box::new(TypeSchema::String)),
comment: "Optional connected account id. Backend resolves from slug toolkit when omitted.",
required: false,
},
FieldSchema {
name: "trigger_config",
ty: TypeSchema::Option(Box::new(TypeSchema::Json)),
comment:
"Trigger config object. For GitHub, include owner/repo or repoFullName.",
required: false,
},
],
outputs: vec![FieldSchema {
name: "result",
ty: TypeSchema::Json,
comment: "Payload: { triggerId, status? }.",
required: true,
}],
},
"get_user_profile" => ControllerSchema {
namespace: "composio",
function: "get_user_profile",
@@ -327,6 +403,36 @@ fn handle_execute(params: Map<String, Value>) -> ControllerFuture {
})
}
fn handle_list_github_repos(params: Map<String, Value>) -> ControllerFuture {
Box::pin(async move {
let config = config_rpc::load_config_with_timeout().await?;
let payload: ListGithubReposParams = serde_json::from_value(Value::Object(params))
.map_err(|e| format!("invalid params: {e}"))?;
to_json(super::ops::composio_list_github_repos(&config, payload.connection_id).await?)
})
}
fn handle_create_trigger(params: Map<String, Value>) -> ControllerFuture {
Box::pin(async move {
let config = config_rpc::load_config_with_timeout().await?;
let payload: CreateTriggerParams = serde_json::from_value(Value::Object(params))
.map_err(|e| format!("invalid params: {e}"))?;
let slug = payload.slug.trim();
if slug.is_empty() {
return Err("invalid params: 'slug' must not be empty".to_string());
}
to_json(
super::ops::composio_create_trigger(
&config,
slug,
payload.connection_id,
payload.trigger_config,
)
.await?,
)
})
}
fn handle_list_trigger_history(params: Map<String, Value>) -> ControllerFuture {
Box::pin(async move {
let config = config_rpc::load_config_with_timeout().await?;
+35
View File
@@ -114,6 +114,41 @@ pub struct ComposioExecuteResponse {
pub cost_usd: f64,
}
// ── GitHub repos + triggers ─────────────────────────────────────────
/// One repository returned by `GET /agent-integrations/composio/github/repos`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComposioGithubRepo {
pub owner: String,
pub repo: String,
#[serde(rename = "fullName")]
pub full_name: String,
#[serde(default)]
pub private: Option<bool>,
#[serde(rename = "defaultBranch", default)]
pub default_branch: Option<String>,
#[serde(rename = "htmlUrl", default)]
pub html_url: Option<String>,
}
/// Response body of `GET /agent-integrations/composio/github/repos`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComposioGithubReposResponse {
#[serde(rename = "connectionId")]
pub connection_id: String,
#[serde(default, rename = "repositories")]
pub repositories: Vec<ComposioGithubRepo>,
}
/// Response body of `POST /agent-integrations/composio/triggers`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComposioCreateTriggerResponse {
#[serde(rename = "triggerId")]
pub trigger_id: String,
#[serde(default)]
pub status: Option<String>,
}
// ── Triggers ────────────────────────────────────────────────────────
/// Payload of the `composio:trigger` Socket.IO event emitted by the backend
+7
View File
@@ -127,6 +127,13 @@ pub(super) fn handle_sio_event(
"[socket] composio:trigger missing toolkit/trigger; dropping event"
);
} else {
log::info!(
"[socket] Publishing composio:trigger to event bus: toolkit={}, trigger={}, metadata_id={}, metadata_uuid={}",
event.toolkit,
event.trigger,
event.metadata.id,
event.metadata.uuid
);
publish_global(DomainEvent::ComposioTriggerReceived {
toolkit: event.toolkit,
trigger: event.trigger,