kubiya-control-plane-api 0.9.15__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (479) hide show
  1. control_plane_api/LICENSE +676 -0
  2. control_plane_api/README.md +350 -0
  3. control_plane_api/__init__.py +4 -0
  4. control_plane_api/__version__.py +8 -0
  5. control_plane_api/alembic/README +1 -0
  6. control_plane_api/alembic/env.py +121 -0
  7. control_plane_api/alembic/script.py.mako +28 -0
  8. control_plane_api/alembic/versions/2613c65c3dbe_initial_database_setup.py +32 -0
  9. control_plane_api/alembic/versions/2df520d4927d_merge_heads.py +28 -0
  10. control_plane_api/alembic/versions/43abf98d6a01_add_paused_status_to_executions.py +73 -0
  11. control_plane_api/alembic/versions/6289854264cb_merge_multiple_heads.py +28 -0
  12. control_plane_api/alembic/versions/6a4d4dc3d8dc_generate_execution_transitions.py +50 -0
  13. control_plane_api/alembic/versions/87d11cf0a783_add_disconnected_status_to_worker_.py +44 -0
  14. control_plane_api/alembic/versions/add_ephemeral_queue_support.py +85 -0
  15. control_plane_api/alembic/versions/add_model_type_to_llm_models.py +31 -0
  16. control_plane_api/alembic/versions/add_plan_executions_table.py +114 -0
  17. control_plane_api/alembic/versions/add_trace_span_tables.py +154 -0
  18. control_plane_api/alembic/versions/add_user_info_to_traces.py +36 -0
  19. control_plane_api/alembic/versions/adjusting_foreign_keys.py +32 -0
  20. control_plane_api/alembic/versions/b4983d976db2_initial_tables.py +1128 -0
  21. control_plane_api/alembic/versions/d181a3b40e71_rename_custom_metadata_to_metadata_in_.py +50 -0
  22. control_plane_api/alembic/versions/df9117888e82_add_missing_columns.py +82 -0
  23. control_plane_api/alembic/versions/f25de6ad895a_missing_migrations.py +34 -0
  24. control_plane_api/alembic/versions/f71305fb69b9_fix_ephemeral_queue_deletion_foreign_key.py +54 -0
  25. control_plane_api/alembic/versions/mark_local_exec_queues_as_ephemeral.py +68 -0
  26. control_plane_api/alembic.ini +148 -0
  27. control_plane_api/api/index.py +12 -0
  28. control_plane_api/app/__init__.py +11 -0
  29. control_plane_api/app/activities/__init__.py +20 -0
  30. control_plane_api/app/activities/agent_activities.py +384 -0
  31. control_plane_api/app/activities/plan_generation_activities.py +499 -0
  32. control_plane_api/app/activities/team_activities.py +424 -0
  33. control_plane_api/app/activities/temporal_cloud_activities.py +588 -0
  34. control_plane_api/app/config/__init__.py +35 -0
  35. control_plane_api/app/config/api_config.py +469 -0
  36. control_plane_api/app/config/config_loader.py +224 -0
  37. control_plane_api/app/config/model_pricing.py +323 -0
  38. control_plane_api/app/config/storage_config.py +159 -0
  39. control_plane_api/app/config.py +115 -0
  40. control_plane_api/app/controllers/__init__.py +0 -0
  41. control_plane_api/app/controllers/execution_environment_controller.py +1315 -0
  42. control_plane_api/app/database.py +135 -0
  43. control_plane_api/app/exceptions.py +408 -0
  44. control_plane_api/app/lib/__init__.py +11 -0
  45. control_plane_api/app/lib/environment.py +65 -0
  46. control_plane_api/app/lib/event_bus/__init__.py +17 -0
  47. control_plane_api/app/lib/event_bus/base.py +136 -0
  48. control_plane_api/app/lib/event_bus/manager.py +335 -0
  49. control_plane_api/app/lib/event_bus/providers/__init__.py +6 -0
  50. control_plane_api/app/lib/event_bus/providers/http_provider.py +166 -0
  51. control_plane_api/app/lib/event_bus/providers/nats_provider.py +324 -0
  52. control_plane_api/app/lib/event_bus/providers/redis_provider.py +233 -0
  53. control_plane_api/app/lib/event_bus/providers/websocket_provider.py +497 -0
  54. control_plane_api/app/lib/job_executor.py +330 -0
  55. control_plane_api/app/lib/kubiya_client.py +293 -0
  56. control_plane_api/app/lib/litellm_pricing.py +166 -0
  57. control_plane_api/app/lib/mcp_validation.py +163 -0
  58. control_plane_api/app/lib/nats/__init__.py +13 -0
  59. control_plane_api/app/lib/nats/credentials_manager.py +288 -0
  60. control_plane_api/app/lib/nats/listener.py +374 -0
  61. control_plane_api/app/lib/planning_prompt_builder.py +153 -0
  62. control_plane_api/app/lib/planning_tools/__init__.py +41 -0
  63. control_plane_api/app/lib/planning_tools/agents.py +409 -0
  64. control_plane_api/app/lib/planning_tools/agno_toolkit.py +836 -0
  65. control_plane_api/app/lib/planning_tools/base.py +119 -0
  66. control_plane_api/app/lib/planning_tools/cognitive_memory_tools.py +403 -0
  67. control_plane_api/app/lib/planning_tools/context_graph_tools.py +545 -0
  68. control_plane_api/app/lib/planning_tools/environments.py +218 -0
  69. control_plane_api/app/lib/planning_tools/knowledge.py +204 -0
  70. control_plane_api/app/lib/planning_tools/models.py +93 -0
  71. control_plane_api/app/lib/planning_tools/planning_service.py +646 -0
  72. control_plane_api/app/lib/planning_tools/resources.py +242 -0
  73. control_plane_api/app/lib/planning_tools/teams.py +334 -0
  74. control_plane_api/app/lib/policy_enforcer_client.py +1016 -0
  75. control_plane_api/app/lib/redis_client.py +803 -0
  76. control_plane_api/app/lib/sqlalchemy_utils.py +486 -0
  77. control_plane_api/app/lib/state_transition_tools/__init__.py +7 -0
  78. control_plane_api/app/lib/state_transition_tools/execution_context.py +388 -0
  79. control_plane_api/app/lib/storage/__init__.py +20 -0
  80. control_plane_api/app/lib/storage/base_provider.py +274 -0
  81. control_plane_api/app/lib/storage/provider_factory.py +157 -0
  82. control_plane_api/app/lib/storage/vercel_blob_provider.py +468 -0
  83. control_plane_api/app/lib/supabase.py +71 -0
  84. control_plane_api/app/lib/supabase_utils.py +138 -0
  85. control_plane_api/app/lib/task_planning/__init__.py +138 -0
  86. control_plane_api/app/lib/task_planning/agent_factory.py +308 -0
  87. control_plane_api/app/lib/task_planning/agents.py +389 -0
  88. control_plane_api/app/lib/task_planning/cache.py +218 -0
  89. control_plane_api/app/lib/task_planning/entity_resolver.py +273 -0
  90. control_plane_api/app/lib/task_planning/helpers.py +293 -0
  91. control_plane_api/app/lib/task_planning/hooks.py +474 -0
  92. control_plane_api/app/lib/task_planning/models.py +503 -0
  93. control_plane_api/app/lib/task_planning/plan_validator.py +166 -0
  94. control_plane_api/app/lib/task_planning/planning_workflow.py +2911 -0
  95. control_plane_api/app/lib/task_planning/runner.py +656 -0
  96. control_plane_api/app/lib/task_planning/streaming_hook.py +213 -0
  97. control_plane_api/app/lib/task_planning/workflow.py +424 -0
  98. control_plane_api/app/lib/templating/__init__.py +88 -0
  99. control_plane_api/app/lib/templating/compiler.py +278 -0
  100. control_plane_api/app/lib/templating/engine.py +178 -0
  101. control_plane_api/app/lib/templating/parsers/__init__.py +29 -0
  102. control_plane_api/app/lib/templating/parsers/base.py +96 -0
  103. control_plane_api/app/lib/templating/parsers/env.py +85 -0
  104. control_plane_api/app/lib/templating/parsers/graph.py +112 -0
  105. control_plane_api/app/lib/templating/parsers/secret.py +87 -0
  106. control_plane_api/app/lib/templating/parsers/simple.py +81 -0
  107. control_plane_api/app/lib/templating/resolver.py +366 -0
  108. control_plane_api/app/lib/templating/types.py +214 -0
  109. control_plane_api/app/lib/templating/validator.py +201 -0
  110. control_plane_api/app/lib/temporal_client.py +232 -0
  111. control_plane_api/app/lib/temporal_credentials_cache.py +178 -0
  112. control_plane_api/app/lib/temporal_credentials_service.py +203 -0
  113. control_plane_api/app/lib/validation/__init__.py +24 -0
  114. control_plane_api/app/lib/validation/runtime_validation.py +388 -0
  115. control_plane_api/app/main.py +531 -0
  116. control_plane_api/app/middleware/__init__.py +10 -0
  117. control_plane_api/app/middleware/auth.py +645 -0
  118. control_plane_api/app/middleware/exception_handler.py +267 -0
  119. control_plane_api/app/middleware/prometheus_middleware.py +173 -0
  120. control_plane_api/app/middleware/rate_limiting.py +384 -0
  121. control_plane_api/app/middleware/request_id.py +202 -0
  122. control_plane_api/app/models/__init__.py +40 -0
  123. control_plane_api/app/models/agent.py +90 -0
  124. control_plane_api/app/models/analytics.py +206 -0
  125. control_plane_api/app/models/associations.py +107 -0
  126. control_plane_api/app/models/auth_user.py +73 -0
  127. control_plane_api/app/models/context.py +161 -0
  128. control_plane_api/app/models/custom_integration.py +99 -0
  129. control_plane_api/app/models/environment.py +64 -0
  130. control_plane_api/app/models/execution.py +125 -0
  131. control_plane_api/app/models/execution_transition.py +50 -0
  132. control_plane_api/app/models/job.py +159 -0
  133. control_plane_api/app/models/llm_model.py +78 -0
  134. control_plane_api/app/models/orchestration.py +66 -0
  135. control_plane_api/app/models/plan_execution.py +102 -0
  136. control_plane_api/app/models/presence.py +49 -0
  137. control_plane_api/app/models/project.py +61 -0
  138. control_plane_api/app/models/project_management.py +85 -0
  139. control_plane_api/app/models/session.py +29 -0
  140. control_plane_api/app/models/skill.py +155 -0
  141. control_plane_api/app/models/system_tables.py +43 -0
  142. control_plane_api/app/models/task_planning.py +372 -0
  143. control_plane_api/app/models/team.py +86 -0
  144. control_plane_api/app/models/trace.py +257 -0
  145. control_plane_api/app/models/user_profile.py +54 -0
  146. control_plane_api/app/models/worker.py +221 -0
  147. control_plane_api/app/models/workflow.py +161 -0
  148. control_plane_api/app/models/workspace.py +50 -0
  149. control_plane_api/app/observability/__init__.py +177 -0
  150. control_plane_api/app/observability/context_logging.py +475 -0
  151. control_plane_api/app/observability/decorators.py +337 -0
  152. control_plane_api/app/observability/local_span_processor.py +702 -0
  153. control_plane_api/app/observability/metrics.py +303 -0
  154. control_plane_api/app/observability/middleware.py +246 -0
  155. control_plane_api/app/observability/optional.py +115 -0
  156. control_plane_api/app/observability/tracing.py +382 -0
  157. control_plane_api/app/policies/README.md +149 -0
  158. control_plane_api/app/policies/approved_users.rego +62 -0
  159. control_plane_api/app/policies/business_hours.rego +51 -0
  160. control_plane_api/app/policies/rate_limiting.rego +100 -0
  161. control_plane_api/app/policies/tool_enforcement/README.md +336 -0
  162. control_plane_api/app/policies/tool_enforcement/bash_command_validation.rego +71 -0
  163. control_plane_api/app/policies/tool_enforcement/business_hours_enforcement.rego +82 -0
  164. control_plane_api/app/policies/tool_enforcement/mcp_tool_allowlist.rego +58 -0
  165. control_plane_api/app/policies/tool_enforcement/production_safeguards.rego +80 -0
  166. control_plane_api/app/policies/tool_enforcement/role_based_tool_access.rego +44 -0
  167. control_plane_api/app/policies/tool_restrictions.rego +86 -0
  168. control_plane_api/app/routers/__init__.py +4 -0
  169. control_plane_api/app/routers/agents.py +382 -0
  170. control_plane_api/app/routers/agents_v2.py +1598 -0
  171. control_plane_api/app/routers/analytics.py +1310 -0
  172. control_plane_api/app/routers/auth.py +59 -0
  173. control_plane_api/app/routers/client_config.py +57 -0
  174. control_plane_api/app/routers/context_graph.py +561 -0
  175. control_plane_api/app/routers/context_manager.py +577 -0
  176. control_plane_api/app/routers/custom_integrations.py +490 -0
  177. control_plane_api/app/routers/enforcer.py +132 -0
  178. control_plane_api/app/routers/environment_context.py +252 -0
  179. control_plane_api/app/routers/environments.py +761 -0
  180. control_plane_api/app/routers/execution_environment.py +847 -0
  181. control_plane_api/app/routers/executions/__init__.py +28 -0
  182. control_plane_api/app/routers/executions/router.py +286 -0
  183. control_plane_api/app/routers/executions/services/__init__.py +22 -0
  184. control_plane_api/app/routers/executions/services/demo_worker_health.py +156 -0
  185. control_plane_api/app/routers/executions/services/status_service.py +420 -0
  186. control_plane_api/app/routers/executions/services/test_worker_health.py +480 -0
  187. control_plane_api/app/routers/executions/services/worker_health.py +514 -0
  188. control_plane_api/app/routers/executions/streaming/__init__.py +22 -0
  189. control_plane_api/app/routers/executions/streaming/deduplication.py +352 -0
  190. control_plane_api/app/routers/executions/streaming/event_buffer.py +353 -0
  191. control_plane_api/app/routers/executions/streaming/event_formatter.py +964 -0
  192. control_plane_api/app/routers/executions/streaming/history_loader.py +588 -0
  193. control_plane_api/app/routers/executions/streaming/live_source.py +693 -0
  194. control_plane_api/app/routers/executions/streaming/streamer.py +849 -0
  195. control_plane_api/app/routers/executions.py +4888 -0
  196. control_plane_api/app/routers/health.py +165 -0
  197. control_plane_api/app/routers/health_v2.py +394 -0
  198. control_plane_api/app/routers/integration_templates.py +496 -0
  199. control_plane_api/app/routers/integrations.py +287 -0
  200. control_plane_api/app/routers/jobs.py +1809 -0
  201. control_plane_api/app/routers/metrics.py +517 -0
  202. control_plane_api/app/routers/models.py +82 -0
  203. control_plane_api/app/routers/models_v2.py +628 -0
  204. control_plane_api/app/routers/plan_executions.py +1481 -0
  205. control_plane_api/app/routers/plan_generation_async.py +304 -0
  206. control_plane_api/app/routers/policies.py +669 -0
  207. control_plane_api/app/routers/presence.py +234 -0
  208. control_plane_api/app/routers/projects.py +987 -0
  209. control_plane_api/app/routers/runners.py +379 -0
  210. control_plane_api/app/routers/runtimes.py +172 -0
  211. control_plane_api/app/routers/secrets.py +171 -0
  212. control_plane_api/app/routers/skills.py +1010 -0
  213. control_plane_api/app/routers/skills_definitions.py +140 -0
  214. control_plane_api/app/routers/storage.py +456 -0
  215. control_plane_api/app/routers/task_planning.py +611 -0
  216. control_plane_api/app/routers/task_queues.py +650 -0
  217. control_plane_api/app/routers/team_context.py +274 -0
  218. control_plane_api/app/routers/teams.py +1747 -0
  219. control_plane_api/app/routers/templates.py +248 -0
  220. control_plane_api/app/routers/traces.py +571 -0
  221. control_plane_api/app/routers/websocket_client.py +479 -0
  222. control_plane_api/app/routers/websocket_executions_status.py +437 -0
  223. control_plane_api/app/routers/websocket_gateway.py +323 -0
  224. control_plane_api/app/routers/websocket_traces.py +576 -0
  225. control_plane_api/app/routers/worker_queues.py +2555 -0
  226. control_plane_api/app/routers/worker_websocket.py +419 -0
  227. control_plane_api/app/routers/workers.py +1004 -0
  228. control_plane_api/app/routers/workflows.py +204 -0
  229. control_plane_api/app/runtimes/__init__.py +6 -0
  230. control_plane_api/app/runtimes/validation.py +344 -0
  231. control_plane_api/app/schemas/__init__.py +1 -0
  232. control_plane_api/app/schemas/job_schemas.py +302 -0
  233. control_plane_api/app/schemas/mcp_schemas.py +311 -0
  234. control_plane_api/app/schemas/template_schemas.py +133 -0
  235. control_plane_api/app/schemas/trace_schemas.py +168 -0
  236. control_plane_api/app/schemas/worker_queue_observability_schemas.py +165 -0
  237. control_plane_api/app/services/__init__.py +1 -0
  238. control_plane_api/app/services/agno_planning_strategy.py +233 -0
  239. control_plane_api/app/services/agno_service.py +838 -0
  240. control_plane_api/app/services/claude_code_planning_service.py +203 -0
  241. control_plane_api/app/services/context_graph_client.py +224 -0
  242. control_plane_api/app/services/custom_integration_service.py +415 -0
  243. control_plane_api/app/services/integration_resolution_service.py +345 -0
  244. control_plane_api/app/services/litellm_service.py +394 -0
  245. control_plane_api/app/services/plan_generator.py +79 -0
  246. control_plane_api/app/services/planning_strategy.py +66 -0
  247. control_plane_api/app/services/planning_strategy_factory.py +118 -0
  248. control_plane_api/app/services/policy_service.py +615 -0
  249. control_plane_api/app/services/state_transition_service.py +755 -0
  250. control_plane_api/app/services/storage_service.py +593 -0
  251. control_plane_api/app/services/temporal_cloud_provisioning.py +150 -0
  252. control_plane_api/app/services/toolsets/context_graph_skill.py +432 -0
  253. control_plane_api/app/services/trace_retention.py +354 -0
  254. control_plane_api/app/services/worker_queue_metrics_service.py +190 -0
  255. control_plane_api/app/services/workflow_cancellation_manager.py +135 -0
  256. control_plane_api/app/services/workflow_operations_service.py +611 -0
  257. control_plane_api/app/skills/__init__.py +100 -0
  258. control_plane_api/app/skills/base.py +239 -0
  259. control_plane_api/app/skills/builtin/__init__.py +37 -0
  260. control_plane_api/app/skills/builtin/agent_communication/__init__.py +8 -0
  261. control_plane_api/app/skills/builtin/agent_communication/skill.py +246 -0
  262. control_plane_api/app/skills/builtin/code_ingestion/__init__.py +4 -0
  263. control_plane_api/app/skills/builtin/code_ingestion/skill.py +267 -0
  264. control_plane_api/app/skills/builtin/cognitive_memory/__init__.py +4 -0
  265. control_plane_api/app/skills/builtin/cognitive_memory/skill.py +174 -0
  266. control_plane_api/app/skills/builtin/contextual_awareness/__init__.py +4 -0
  267. control_plane_api/app/skills/builtin/contextual_awareness/skill.py +387 -0
  268. control_plane_api/app/skills/builtin/data_visualization/__init__.py +4 -0
  269. control_plane_api/app/skills/builtin/data_visualization/skill.py +154 -0
  270. control_plane_api/app/skills/builtin/docker/__init__.py +4 -0
  271. control_plane_api/app/skills/builtin/docker/skill.py +104 -0
  272. control_plane_api/app/skills/builtin/file_generation/__init__.py +4 -0
  273. control_plane_api/app/skills/builtin/file_generation/skill.py +94 -0
  274. control_plane_api/app/skills/builtin/file_system/__init__.py +4 -0
  275. control_plane_api/app/skills/builtin/file_system/skill.py +110 -0
  276. control_plane_api/app/skills/builtin/knowledge_api/__init__.py +5 -0
  277. control_plane_api/app/skills/builtin/knowledge_api/skill.py +124 -0
  278. control_plane_api/app/skills/builtin/python/__init__.py +4 -0
  279. control_plane_api/app/skills/builtin/python/skill.py +92 -0
  280. control_plane_api/app/skills/builtin/remote_filesystem/__init__.py +5 -0
  281. control_plane_api/app/skills/builtin/remote_filesystem/skill.py +170 -0
  282. control_plane_api/app/skills/builtin/shell/__init__.py +4 -0
  283. control_plane_api/app/skills/builtin/shell/skill.py +161 -0
  284. control_plane_api/app/skills/builtin/slack/__init__.py +3 -0
  285. control_plane_api/app/skills/builtin/slack/skill.py +302 -0
  286. control_plane_api/app/skills/builtin/workflow_executor/__init__.py +4 -0
  287. control_plane_api/app/skills/builtin/workflow_executor/skill.py +469 -0
  288. control_plane_api/app/skills/business_intelligence.py +189 -0
  289. control_plane_api/app/skills/config.py +63 -0
  290. control_plane_api/app/skills/loaders/__init__.py +14 -0
  291. control_plane_api/app/skills/loaders/base.py +73 -0
  292. control_plane_api/app/skills/loaders/filesystem_loader.py +199 -0
  293. control_plane_api/app/skills/registry.py +125 -0
  294. control_plane_api/app/utils/helpers.py +12 -0
  295. control_plane_api/app/utils/workflow_executor.py +354 -0
  296. control_plane_api/app/workflows/__init__.py +11 -0
  297. control_plane_api/app/workflows/agent_execution.py +520 -0
  298. control_plane_api/app/workflows/agent_execution_with_skills.py +223 -0
  299. control_plane_api/app/workflows/namespace_provisioning.py +326 -0
  300. control_plane_api/app/workflows/plan_generation.py +254 -0
  301. control_plane_api/app/workflows/team_execution.py +442 -0
  302. control_plane_api/scripts/seed_models.py +240 -0
  303. control_plane_api/scripts/validate_existing_tool_names.py +492 -0
  304. control_plane_api/shared/__init__.py +8 -0
  305. control_plane_api/shared/version.py +17 -0
  306. control_plane_api/test_deduplication.py +274 -0
  307. control_plane_api/test_executor_deduplication_e2e.py +309 -0
  308. control_plane_api/test_job_execution_e2e.py +283 -0
  309. control_plane_api/test_real_integration.py +193 -0
  310. control_plane_api/version.py +38 -0
  311. control_plane_api/worker/__init__.py +0 -0
  312. control_plane_api/worker/activities/__init__.py +0 -0
  313. control_plane_api/worker/activities/agent_activities.py +1585 -0
  314. control_plane_api/worker/activities/approval_activities.py +234 -0
  315. control_plane_api/worker/activities/job_activities.py +199 -0
  316. control_plane_api/worker/activities/runtime_activities.py +1167 -0
  317. control_plane_api/worker/activities/skill_activities.py +282 -0
  318. control_plane_api/worker/activities/team_activities.py +479 -0
  319. control_plane_api/worker/agent_runtime_server.py +370 -0
  320. control_plane_api/worker/binary_manager.py +333 -0
  321. control_plane_api/worker/config/__init__.py +31 -0
  322. control_plane_api/worker/config/worker_config.py +273 -0
  323. control_plane_api/worker/control_plane_client.py +1491 -0
  324. control_plane_api/worker/examples/analytics_integration_example.py +362 -0
  325. control_plane_api/worker/health_monitor.py +159 -0
  326. control_plane_api/worker/metrics.py +237 -0
  327. control_plane_api/worker/models/__init__.py +1 -0
  328. control_plane_api/worker/models/error_events.py +105 -0
  329. control_plane_api/worker/models/inputs.py +89 -0
  330. control_plane_api/worker/runtimes/__init__.py +35 -0
  331. control_plane_api/worker/runtimes/agent_runtime/runtime.py +485 -0
  332. control_plane_api/worker/runtimes/agno/__init__.py +34 -0
  333. control_plane_api/worker/runtimes/agno/config.py +248 -0
  334. control_plane_api/worker/runtimes/agno/hooks.py +385 -0
  335. control_plane_api/worker/runtimes/agno/mcp_builder.py +195 -0
  336. control_plane_api/worker/runtimes/agno/runtime.py +1063 -0
  337. control_plane_api/worker/runtimes/agno/utils.py +163 -0
  338. control_plane_api/worker/runtimes/base.py +979 -0
  339. control_plane_api/worker/runtimes/claude_code/__init__.py +38 -0
  340. control_plane_api/worker/runtimes/claude_code/cleanup.py +184 -0
  341. control_plane_api/worker/runtimes/claude_code/client_pool.py +529 -0
  342. control_plane_api/worker/runtimes/claude_code/config.py +829 -0
  343. control_plane_api/worker/runtimes/claude_code/hooks.py +482 -0
  344. control_plane_api/worker/runtimes/claude_code/litellm_proxy.py +1702 -0
  345. control_plane_api/worker/runtimes/claude_code/mcp_builder.py +467 -0
  346. control_plane_api/worker/runtimes/claude_code/mcp_discovery.py +558 -0
  347. control_plane_api/worker/runtimes/claude_code/runtime.py +1546 -0
  348. control_plane_api/worker/runtimes/claude_code/tool_mapper.py +403 -0
  349. control_plane_api/worker/runtimes/claude_code/utils.py +149 -0
  350. control_plane_api/worker/runtimes/factory.py +173 -0
  351. control_plane_api/worker/runtimes/model_utils.py +107 -0
  352. control_plane_api/worker/runtimes/validation.py +93 -0
  353. control_plane_api/worker/services/__init__.py +1 -0
  354. control_plane_api/worker/services/agent_communication_tools.py +908 -0
  355. control_plane_api/worker/services/agent_executor.py +485 -0
  356. control_plane_api/worker/services/agent_executor_v2.py +793 -0
  357. control_plane_api/worker/services/analytics_collector.py +457 -0
  358. control_plane_api/worker/services/analytics_service.py +464 -0
  359. control_plane_api/worker/services/approval_tools.py +310 -0
  360. control_plane_api/worker/services/approval_tools_agno.py +207 -0
  361. control_plane_api/worker/services/cancellation_manager.py +177 -0
  362. control_plane_api/worker/services/code_ingestion_tools.py +465 -0
  363. control_plane_api/worker/services/contextual_awareness_tools.py +405 -0
  364. control_plane_api/worker/services/data_visualization.py +834 -0
  365. control_plane_api/worker/services/event_publisher.py +531 -0
  366. control_plane_api/worker/services/jira_tools.py +257 -0
  367. control_plane_api/worker/services/remote_filesystem_tools.py +498 -0
  368. control_plane_api/worker/services/runtime_analytics.py +328 -0
  369. control_plane_api/worker/services/session_service.py +365 -0
  370. control_plane_api/worker/services/skill_context_enhancement.py +181 -0
  371. control_plane_api/worker/services/skill_factory.py +471 -0
  372. control_plane_api/worker/services/system_prompt_enhancement.py +410 -0
  373. control_plane_api/worker/services/team_executor.py +715 -0
  374. control_plane_api/worker/services/team_executor_v2.py +1866 -0
  375. control_plane_api/worker/services/tool_enforcement.py +254 -0
  376. control_plane_api/worker/services/workflow_executor/__init__.py +52 -0
  377. control_plane_api/worker/services/workflow_executor/event_processor.py +287 -0
  378. control_plane_api/worker/services/workflow_executor/event_publisher.py +210 -0
  379. control_plane_api/worker/services/workflow_executor/executors/__init__.py +15 -0
  380. control_plane_api/worker/services/workflow_executor/executors/base.py +270 -0
  381. control_plane_api/worker/services/workflow_executor/executors/json_executor.py +50 -0
  382. control_plane_api/worker/services/workflow_executor/executors/python_executor.py +50 -0
  383. control_plane_api/worker/services/workflow_executor/models.py +142 -0
  384. control_plane_api/worker/services/workflow_executor_tools.py +1748 -0
  385. control_plane_api/worker/skills/__init__.py +12 -0
  386. control_plane_api/worker/skills/builtin/context_graph_search/README.md +213 -0
  387. control_plane_api/worker/skills/builtin/context_graph_search/__init__.py +5 -0
  388. control_plane_api/worker/skills/builtin/context_graph_search/agno_impl.py +808 -0
  389. control_plane_api/worker/skills/builtin/context_graph_search/skill.yaml +67 -0
  390. control_plane_api/worker/skills/builtin/contextual_awareness/__init__.py +4 -0
  391. control_plane_api/worker/skills/builtin/contextual_awareness/agno_impl.py +62 -0
  392. control_plane_api/worker/skills/builtin/data_visualization/agno_impl.py +18 -0
  393. control_plane_api/worker/skills/builtin/data_visualization/skill.yaml +84 -0
  394. control_plane_api/worker/skills/builtin/docker/agno_impl.py +65 -0
  395. control_plane_api/worker/skills/builtin/docker/skill.yaml +60 -0
  396. control_plane_api/worker/skills/builtin/file_generation/agno_impl.py +47 -0
  397. control_plane_api/worker/skills/builtin/file_generation/skill.yaml +64 -0
  398. control_plane_api/worker/skills/builtin/file_system/agno_impl.py +32 -0
  399. control_plane_api/worker/skills/builtin/file_system/skill.yaml +54 -0
  400. control_plane_api/worker/skills/builtin/knowledge_api/__init__.py +4 -0
  401. control_plane_api/worker/skills/builtin/knowledge_api/agno_impl.py +50 -0
  402. control_plane_api/worker/skills/builtin/knowledge_api/skill.yaml +66 -0
  403. control_plane_api/worker/skills/builtin/python/agno_impl.py +25 -0
  404. control_plane_api/worker/skills/builtin/python/skill.yaml +60 -0
  405. control_plane_api/worker/skills/builtin/schema_fix_mixin.py +260 -0
  406. control_plane_api/worker/skills/builtin/shell/agno_impl.py +31 -0
  407. control_plane_api/worker/skills/builtin/shell/skill.yaml +60 -0
  408. control_plane_api/worker/skills/builtin/slack/__init__.py +3 -0
  409. control_plane_api/worker/skills/builtin/slack/agno_impl.py +1282 -0
  410. control_plane_api/worker/skills/builtin/slack/skill.yaml +276 -0
  411. control_plane_api/worker/skills/builtin/workflow_executor/agno_impl.py +62 -0
  412. control_plane_api/worker/skills/builtin/workflow_executor/skill.yaml +79 -0
  413. control_plane_api/worker/skills/loaders/__init__.py +5 -0
  414. control_plane_api/worker/skills/loaders/base.py +23 -0
  415. control_plane_api/worker/skills/loaders/filesystem_loader.py +357 -0
  416. control_plane_api/worker/skills/registry.py +208 -0
  417. control_plane_api/worker/tests/__init__.py +1 -0
  418. control_plane_api/worker/tests/conftest.py +12 -0
  419. control_plane_api/worker/tests/e2e/__init__.py +0 -0
  420. control_plane_api/worker/tests/e2e/test_context_graph_real_api.py +338 -0
  421. control_plane_api/worker/tests/e2e/test_context_graph_templates_e2e.py +523 -0
  422. control_plane_api/worker/tests/e2e/test_enforcement_e2e.py +344 -0
  423. control_plane_api/worker/tests/e2e/test_execution_flow.py +571 -0
  424. control_plane_api/worker/tests/e2e/test_single_execution_mode.py +656 -0
  425. control_plane_api/worker/tests/integration/__init__.py +0 -0
  426. control_plane_api/worker/tests/integration/test_builtin_skills_fixes.py +245 -0
  427. control_plane_api/worker/tests/integration/test_context_graph_search_integration.py +365 -0
  428. control_plane_api/worker/tests/integration/test_control_plane_integration.py +308 -0
  429. control_plane_api/worker/tests/integration/test_hook_enforcement_integration.py +579 -0
  430. control_plane_api/worker/tests/integration/test_scheduled_job_workflow.py +237 -0
  431. control_plane_api/worker/tests/integration/test_system_prompt_enhancement_integration.py +343 -0
  432. control_plane_api/worker/tests/unit/__init__.py +0 -0
  433. control_plane_api/worker/tests/unit/test_builtin_skill_autoload.py +396 -0
  434. control_plane_api/worker/tests/unit/test_context_graph_search.py +450 -0
  435. control_plane_api/worker/tests/unit/test_context_graph_templates.py +403 -0
  436. control_plane_api/worker/tests/unit/test_control_plane_client.py +401 -0
  437. control_plane_api/worker/tests/unit/test_control_plane_client_jobs.py +345 -0
  438. control_plane_api/worker/tests/unit/test_job_activities.py +353 -0
  439. control_plane_api/worker/tests/unit/test_skill_context_enhancement.py +321 -0
  440. control_plane_api/worker/tests/unit/test_system_prompt_enhancement.py +415 -0
  441. control_plane_api/worker/tests/unit/test_tool_enforcement.py +324 -0
  442. control_plane_api/worker/utils/__init__.py +1 -0
  443. control_plane_api/worker/utils/chunk_batcher.py +330 -0
  444. control_plane_api/worker/utils/environment.py +65 -0
  445. control_plane_api/worker/utils/error_publisher.py +260 -0
  446. control_plane_api/worker/utils/event_batcher.py +256 -0
  447. control_plane_api/worker/utils/logging_config.py +335 -0
  448. control_plane_api/worker/utils/logging_helper.py +326 -0
  449. control_plane_api/worker/utils/parameter_validator.py +120 -0
  450. control_plane_api/worker/utils/retry_utils.py +60 -0
  451. control_plane_api/worker/utils/streaming_utils.py +665 -0
  452. control_plane_api/worker/utils/tool_validation.py +332 -0
  453. control_plane_api/worker/utils/workspace_manager.py +163 -0
  454. control_plane_api/worker/websocket_client.py +393 -0
  455. control_plane_api/worker/worker.py +1297 -0
  456. control_plane_api/worker/workflows/__init__.py +0 -0
  457. control_plane_api/worker/workflows/agent_execution.py +909 -0
  458. control_plane_api/worker/workflows/scheduled_job_wrapper.py +332 -0
  459. control_plane_api/worker/workflows/team_execution.py +611 -0
  460. kubiya_control_plane_api-0.9.15.dist-info/METADATA +354 -0
  461. kubiya_control_plane_api-0.9.15.dist-info/RECORD +479 -0
  462. kubiya_control_plane_api-0.9.15.dist-info/WHEEL +5 -0
  463. kubiya_control_plane_api-0.9.15.dist-info/entry_points.txt +5 -0
  464. kubiya_control_plane_api-0.9.15.dist-info/licenses/LICENSE +676 -0
  465. kubiya_control_plane_api-0.9.15.dist-info/top_level.txt +3 -0
  466. scripts/__init__.py +1 -0
  467. scripts/migrations.py +39 -0
  468. scripts/seed_worker_queues.py +128 -0
  469. scripts/setup_agent_runtime.py +142 -0
  470. worker_internal/__init__.py +1 -0
  471. worker_internal/planner/__init__.py +1 -0
  472. worker_internal/planner/activities.py +1499 -0
  473. worker_internal/planner/agent_tools.py +197 -0
  474. worker_internal/planner/event_models.py +148 -0
  475. worker_internal/planner/event_publisher.py +67 -0
  476. worker_internal/planner/models.py +199 -0
  477. worker_internal/planner/retry_logic.py +134 -0
  478. worker_internal/planner/worker.py +300 -0
  479. worker_internal/planner/workflows.py +970 -0
@@ -0,0 +1,1282 @@
1
+ """
2
+ Slack Tools - Integration with OAuth support and lazy validation
3
+
4
+ Provides comprehensive Slack API access with configuration-driven capabilities.
5
+ """
6
+ import os
7
+ import json
8
+ import logging
9
+ from typing import Optional, Dict, Any, List
10
+ from control_plane_api.worker.skills.builtin.schema_fix_mixin import SchemaFixMixin
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ try:
15
+ from agno.tools import Toolkit
16
+ except ImportError:
17
+ # Fallback for testing
18
+ class Toolkit:
19
+ def __init__(self, name: str):
20
+ self.name = name
21
+ self._tools = []
22
+
23
+ def register(self, func):
24
+ self._tools.append(func)
25
+
26
+ # Fix: Rebuild function schemas with proper parameters
27
+ self._rebuild_function_schemas()
28
+ class SlackTools(SchemaFixMixin, Toolkit):
29
+ """
30
+ Slack integration toolkit with OAuth support via Integration API.
31
+
32
+ Authentication (priority order):
33
+ 1. Integration API (fetch token via /integrations/slack/{id}/token)
34
+ 2. Secret from vault (secret_name resolved as environment variable)
35
+ 3. SLACK_BOT_TOKEN environment variable (fallback)
36
+
37
+ Lazy validation pattern: Don't fail on __init__, validate on first tool use.
38
+
39
+ Capabilities are configuration-driven with fine-grained permission control.
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ integration_id: Optional[str] = None,
45
+ secret_name: Optional[str] = None, # Deprecated, use secrets
46
+ secrets: Optional[List[str]] = None,
47
+ use_env_fallback: bool = True,
48
+ enable_read_messages: bool = True,
49
+ enable_write_messages: bool = False,
50
+ enable_reactions: bool = False,
51
+ enable_channel_management: bool = False,
52
+ # Fine-grained read permissions
53
+ read_channels: bool = True,
54
+ read_messages: bool = True,
55
+ search_messages: bool = True,
56
+ read_threads: bool = True,
57
+ read_users: bool = True,
58
+ # Fine-grained write permissions
59
+ send_messages: bool = True,
60
+ post_messages: bool = True,
61
+ reply_to_threads: bool = True,
62
+ update_messages: bool = True,
63
+ # Fine-grained reaction permissions
64
+ add_reactions: bool = True,
65
+ remove_reactions: bool = True,
66
+ # Fine-grained channel management permissions
67
+ create_channels: bool = False,
68
+ invite_to_channels: bool = False,
69
+ archive_channels: bool = False,
70
+ set_channel_topic: bool = False,
71
+ # General settings
72
+ timeout: int = 30,
73
+ default_channel: Optional[str] = None,
74
+ # Channel scoping
75
+ allowed_channels: Optional[List[str]] = None,
76
+ blocked_channels: Optional[List[str]] = None,
77
+ **kwargs
78
+ ):
79
+ super().__init__(name="slack")
80
+
81
+ # Store authentication config
82
+ self.integration_id = integration_id
83
+ # Handle backwards compatibility: secret_name -> secrets list
84
+ if secret_name:
85
+ self.secrets = [secret_name]
86
+ elif secrets:
87
+ self.secrets = secrets if isinstance(secrets, list) else [secrets]
88
+ else:
89
+ self.secrets = []
90
+ self.use_env_fallback = use_env_fallback
91
+ self.timeout = timeout
92
+ self.default_channel = default_channel
93
+
94
+ # Store channel scoping
95
+ self.allowed_channels = allowed_channels or []
96
+ self.blocked_channels = blocked_channels or []
97
+
98
+ # Store capability flags
99
+ self.capabilities = {
100
+ "read_messages": enable_read_messages,
101
+ "write_messages": enable_write_messages,
102
+ "reactions": enable_reactions,
103
+ "channel_management": enable_channel_management,
104
+ }
105
+
106
+ # Store fine-grained permissions
107
+ self.permissions = {
108
+ "read_channels": read_channels,
109
+ "read_messages": read_messages,
110
+ "search_messages": search_messages,
111
+ "read_threads": read_threads,
112
+ "read_users": read_users,
113
+ "send_messages": send_messages,
114
+ "post_messages": post_messages,
115
+ "reply_to_threads": reply_to_threads,
116
+ "update_messages": update_messages,
117
+ "add_reactions": add_reactions,
118
+ "remove_reactions": remove_reactions,
119
+ "create_channels": create_channels,
120
+ "invite_to_channels": invite_to_channels,
121
+ "archive_channels": archive_channels,
122
+ "set_channel_topic": set_channel_topic,
123
+ }
124
+
125
+ # Lazy initialization - don't fetch token or create client yet
126
+ self._client = None
127
+ self._token = None
128
+ self._auth_checked = False
129
+
130
+ # Register tools based on enabled capabilities
131
+ self._register_tools()
132
+
133
+ def _is_channel_allowed(self, channel_id: str, channel_name: str = None) -> tuple[bool, Optional[str]]:
134
+ """
135
+ Check if a channel is allowed based on scoping configuration.
136
+
137
+ Args:
138
+ channel_id: Channel ID (e.g., "C01234567")
139
+ channel_name: Optional channel name (e.g., "general", "#general")
140
+
141
+ Returns:
142
+ Tuple of (is_allowed, error_message)
143
+ """
144
+ # Normalize channel name (remove # prefix if present)
145
+ if channel_name:
146
+ channel_name = channel_name.lstrip('#')
147
+
148
+ # If no restrictions, allow all
149
+ if not self.allowed_channels and not self.blocked_channels:
150
+ return True, None
151
+
152
+ # Check blocked channels first
153
+ if self.blocked_channels:
154
+ if channel_id in self.blocked_channels:
155
+ return False, f"Channel {channel_id} is blocked by configuration"
156
+ if channel_name and channel_name in self.blocked_channels:
157
+ return False, f"Channel {channel_name} is blocked by configuration"
158
+
159
+ # Check allowed channels
160
+ if self.allowed_channels:
161
+ if channel_id not in self.allowed_channels:
162
+ if not channel_name or channel_name not in self.allowed_channels:
163
+ return False, f"Channel not in allowed list. Allowed channels: {', '.join(self.allowed_channels)}"
164
+
165
+ return True, None
166
+
167
+ def _register_tools(self):
168
+ """Register tools based on enabled capabilities and permissions."""
169
+ # Always register get_available_channels (useful for discovery)
170
+ self.register(self.get_available_channels)
171
+
172
+ # Read Messages Group
173
+ if self.capabilities["read_messages"]:
174
+ if self.permissions["read_channels"]:
175
+ self.register(self.list_channels)
176
+ if self.permissions["read_messages"]:
177
+ self.register(self.get_channel_history)
178
+ if self.permissions["search_messages"]:
179
+ self.register(self.search_messages)
180
+ if self.permissions["read_threads"]:
181
+ self.register(self.get_thread_replies)
182
+ if self.permissions["read_users"]:
183
+ self.register(self.get_user_info)
184
+ self.register(self.list_users)
185
+
186
+ # Write Messages Group
187
+ if self.capabilities["write_messages"]:
188
+ if self.permissions["send_messages"]:
189
+ self.register(self.send_message)
190
+ if self.permissions["post_messages"]:
191
+ self.register(self.post_message)
192
+ if self.permissions["reply_to_threads"]:
193
+ self.register(self.reply_to_thread)
194
+ if self.permissions["update_messages"]:
195
+ self.register(self.update_message)
196
+ self.register(self.delete_message)
197
+
198
+ # Reactions Group
199
+ if self.capabilities["reactions"]:
200
+ if self.permissions["add_reactions"]:
201
+ self.register(self.add_reaction)
202
+ if self.permissions["remove_reactions"]:
203
+ self.register(self.remove_reaction)
204
+ self.register(self.list_reactions)
205
+
206
+ # Channel Management Group
207
+ if self.capabilities["channel_management"]:
208
+ if self.permissions["create_channels"]:
209
+ self.register(self.create_channel)
210
+ if self.permissions["invite_to_channels"]:
211
+ self.register(self.invite_to_channel)
212
+ if self.permissions["archive_channels"]:
213
+ self.register(self.archive_channel)
214
+ if self.permissions["set_channel_topic"]:
215
+ self.register(self.set_channel_topic)
216
+ self.register(self.set_channel_description)
217
+
218
+ def _get_client(self):
219
+ """Lazy initialization of Slack client with authentication."""
220
+ if self._client is not None:
221
+ return self._client
222
+
223
+ # Get authentication token
224
+ token = self._get_auth_token()
225
+
226
+ # Import slack_sdk
227
+ try:
228
+ from slack_sdk import WebClient
229
+ except ImportError:
230
+ raise ImportError(
231
+ "slack_sdk is required for Slack skill. "
232
+ "Install with: pip install slack-sdk"
233
+ )
234
+
235
+ # Create client
236
+ self._client = WebClient(token=token, timeout=self.timeout)
237
+ logger.info("Slack WebClient initialized successfully")
238
+ return self._client
239
+
240
+ def _get_auth_token(self) -> str:
241
+ """
242
+ Get Slack authentication token.
243
+
244
+ Priority:
245
+ 1. Integration API (if integration_id provided)
246
+ 2. Secrets from vault (if secrets list provided, resolved as env vars)
247
+ 3. Environment variable SLACK_BOT_TOKEN (if use_env_fallback=True)
248
+
249
+ Returns OAuth URL if integration exists but not authenticated.
250
+ Raises exception if no authentication available.
251
+ """
252
+ if self._token:
253
+ return self._token
254
+
255
+ # Try Integration API first
256
+ if self.integration_id:
257
+ logger.info(f"Attempting to fetch Slack token from Integration API for integration_id: {self.integration_id}")
258
+ token = self._fetch_from_integration_api()
259
+ if token:
260
+ self._token = token
261
+ return token
262
+
263
+ # If integration exists but no token, return OAuth URL
264
+ oauth_url = self._get_oauth_url()
265
+ if oauth_url:
266
+ logger.warning(f"Slack integration not authenticated, OAuth URL: {oauth_url}")
267
+ raise Exception(
268
+ f"❌ Slack integration not authenticated.\n\n"
269
+ f"🔗 Please complete OAuth flow:\n{oauth_url}\n\n"
270
+ f"After authorizing, please retry your request."
271
+ )
272
+
273
+ # Try secrets from vault (resolved as environment variables)
274
+ # Check each secret in order, use first one found
275
+ if self.secrets:
276
+ for secret_name in self.secrets:
277
+ # Secrets from the vault are injected as environment variables
278
+ # by the execution environment controller with the secret name as the key
279
+ token = os.environ.get(secret_name)
280
+ if token:
281
+ logger.info(f"Using Slack token from secret: {secret_name}")
282
+ self._token = token
283
+ return token
284
+
285
+ # None of the secrets were found
286
+ logger.warning(f"None of the configured secrets found in environment: {self.secrets}")
287
+
288
+ # Try environment variable fallback
289
+ if self.use_env_fallback:
290
+ token = os.environ.get("SLACK_BOT_TOKEN")
291
+ if token:
292
+ logger.info("Using Slack token from SLACK_BOT_TOKEN environment variable")
293
+ self._token = token
294
+ return token
295
+
296
+ # No authentication available
297
+ logger.error("No Slack authentication found")
298
+ raise Exception(
299
+ "❌ No Slack authentication found.\n\n"
300
+ "Either provide integration_id with authenticated OAuth, "
301
+ "provide secrets referencing secrets in the vault, "
302
+ "or set SLACK_BOT_TOKEN environment variable."
303
+ )
304
+
305
+ def _fetch_from_integration_api(self) -> Optional[str]:
306
+ """Fetch token from Integration API."""
307
+ try:
308
+ import httpx
309
+
310
+ # Get integration API base URL from environment
311
+ integration_api_base = os.environ.get(
312
+ "INTEGRATION_API_BASE",
313
+ "https://api.kubiya.ai"
314
+ )
315
+ api_key = os.environ.get("KUBIYA_API_KEY")
316
+
317
+ if not api_key:
318
+ logger.warning("KUBIYA_API_KEY not set, cannot fetch from Integration API")
319
+ return None
320
+
321
+ # Call Integration API
322
+ url = f"{integration_api_base}/integrations/slack/{self.integration_id}/token"
323
+ headers = {
324
+ "Authorization": f"Bearer {api_key}",
325
+ "Accept": "application/json"
326
+ }
327
+
328
+ logger.debug(f"Fetching Slack token from: {url}")
329
+ response = httpx.get(url, headers=headers, timeout=self.timeout)
330
+ response.raise_for_status()
331
+
332
+ data = response.json()
333
+ token = data.get("access_token")
334
+
335
+ if token:
336
+ logger.info("Successfully fetched Slack token from Integration API")
337
+ return token
338
+
339
+ logger.warning("No access_token in Integration API response")
340
+ return None
341
+
342
+ except Exception as e:
343
+ logger.error(f"Failed to fetch Slack token from Integration API: {e}")
344
+ return None
345
+
346
+ def _get_oauth_url(self) -> Optional[str]:
347
+ """Get OAuth URL for Slack integration."""
348
+ try:
349
+ import httpx
350
+
351
+ integration_api_base = os.environ.get(
352
+ "INTEGRATION_API_BASE",
353
+ "https://api.kubiya.ai"
354
+ )
355
+ api_key = os.environ.get("KUBIYA_API_KEY")
356
+
357
+ if not api_key:
358
+ return None
359
+
360
+ url = f"{integration_api_base}/integrations/slack/{self.integration_id}/oauth-url"
361
+ headers = {
362
+ "Authorization": f"Bearer {api_key}",
363
+ "Accept": "application/json"
364
+ }
365
+
366
+ response = httpx.get(url, headers=headers, timeout=self.timeout)
367
+ response.raise_for_status()
368
+
369
+ data = response.json()
370
+ return data.get("oauth_url")
371
+
372
+ except Exception as e:
373
+ logger.error(f"Failed to get OAuth URL: {e}")
374
+ return None
375
+
376
+ # ========================================================================
377
+ # Channel Discovery & Scoping
378
+ # ========================================================================
379
+
380
+ def get_available_channels(
381
+ self,
382
+ types: str = "public_channel,private_channel",
383
+ limit: int = 100
384
+ ) -> str:
385
+ """
386
+ Get list of channels available to this skill based on configuration.
387
+
388
+ This tool respects the allowed_channels and blocked_channels configuration,
389
+ returning only channels that the skill is permitted to access.
390
+
391
+ Args:
392
+ types: Channel types (public_channel,private_channel,mpim,im)
393
+ limit: Maximum channels to return (default: 100)
394
+
395
+ Returns:
396
+ JSON string with available channels and scoping info
397
+ """
398
+ try:
399
+ client = self._get_client()
400
+ response = client.conversations_list(
401
+ types=types,
402
+ limit=min(limit, 1000)
403
+ )
404
+
405
+ all_channels = response.get("channels", [])
406
+
407
+ # Filter channels based on scoping configuration
408
+ available_channels = []
409
+ blocked_count = 0
410
+
411
+ for ch in all_channels:
412
+ channel_id = ch["id"]
413
+ channel_name = ch.get("name")
414
+
415
+ # Check if channel is allowed
416
+ is_allowed, error_msg = self._is_channel_allowed(channel_id, channel_name)
417
+
418
+ if is_allowed:
419
+ available_channels.append({
420
+ "id": ch["id"],
421
+ "name": ch.get("name"),
422
+ "is_private": ch.get("is_private", False),
423
+ "is_archived": ch.get("is_archived", False),
424
+ "is_member": ch.get("is_member", False),
425
+ "num_members": ch.get("num_members", 0),
426
+ "topic": ch.get("topic", {}).get("value", ""),
427
+ "purpose": ch.get("purpose", {}).get("value", "")
428
+ })
429
+ else:
430
+ blocked_count += 1
431
+
432
+ result = {
433
+ "success": True,
434
+ "available_channels": available_channels,
435
+ "available_count": len(available_channels),
436
+ "total_channels": len(all_channels),
437
+ "blocked_count": blocked_count,
438
+ "scoping_active": bool(self.allowed_channels or self.blocked_channels),
439
+ "allowed_channels_config": self.allowed_channels,
440
+ "blocked_channels_config": self.blocked_channels,
441
+ "message": (
442
+ f"Found {len(available_channels)} available channels "
443
+ f"(blocked {blocked_count} due to configuration)"
444
+ if blocked_count > 0
445
+ else f"Found {len(available_channels)} available channels"
446
+ )
447
+ }
448
+ return json.dumps(result, indent=2)
449
+
450
+ except Exception as e:
451
+ logger.error(f"Error getting available channels: {e}")
452
+ return json.dumps({
453
+ "success": False,
454
+ "error": str(e)
455
+ })
456
+
457
+ # ========================================================================
458
+ # Read Messages Group
459
+ # ========================================================================
460
+
461
+ def list_channels(
462
+ self,
463
+ types: str = "public_channel,private_channel",
464
+ limit: int = 100
465
+ ) -> str:
466
+ """
467
+ List Slack channels.
468
+
469
+ Args:
470
+ types: Channel types (public_channel,private_channel,mpim,im)
471
+ limit: Maximum channels to return (default: 100)
472
+
473
+ Returns:
474
+ JSON string with channel list
475
+ """
476
+ try:
477
+ client = self._get_client()
478
+ response = client.conversations_list(
479
+ types=types,
480
+ limit=min(limit, 1000)
481
+ )
482
+
483
+ channels = response.get("channels", [])
484
+ result = {
485
+ "success": True,
486
+ "channels": [
487
+ {
488
+ "id": ch["id"],
489
+ "name": ch.get("name"),
490
+ "is_private": ch.get("is_private", False),
491
+ "is_archived": ch.get("is_archived", False),
492
+ "is_member": ch.get("is_member", False),
493
+ "num_members": ch.get("num_members", 0),
494
+ "topic": ch.get("topic", {}).get("value", ""),
495
+ "purpose": ch.get("purpose", {}).get("value", "")
496
+ }
497
+ for ch in channels
498
+ ],
499
+ "count": len(channels)
500
+ }
501
+ return json.dumps(result, indent=2)
502
+
503
+ except Exception as e:
504
+ logger.error(f"Error listing channels: {e}")
505
+ return json.dumps({
506
+ "success": False,
507
+ "error": str(e)
508
+ })
509
+
510
+ def get_channel_history(
511
+ self,
512
+ channel_id: str,
513
+ limit: int = 50,
514
+ oldest: Optional[str] = None,
515
+ latest: Optional[str] = None
516
+ ) -> str:
517
+ """
518
+ Get message history from a Slack channel.
519
+
520
+ Args:
521
+ channel_id: Channel ID (required)
522
+ limit: Number of messages (default: 50, max: 1000)
523
+ oldest: Timestamp for oldest message
524
+ latest: Timestamp for latest message
525
+
526
+ Returns:
527
+ JSON string with message history
528
+ """
529
+ try:
530
+ client = self._get_client()
531
+ kwargs = {
532
+ "channel": channel_id,
533
+ "limit": min(limit, 1000)
534
+ }
535
+ if oldest:
536
+ kwargs["oldest"] = oldest
537
+ if latest:
538
+ kwargs["latest"] = latest
539
+
540
+ response = client.conversations_history(**kwargs)
541
+
542
+ messages = response.get("messages", [])
543
+ result = {
544
+ "success": True,
545
+ "channel_id": channel_id,
546
+ "messages": [
547
+ {
548
+ "ts": msg.get("ts"),
549
+ "user": msg.get("user"),
550
+ "text": msg.get("text"),
551
+ "type": msg.get("type"),
552
+ "thread_ts": msg.get("thread_ts"),
553
+ "reply_count": msg.get("reply_count", 0),
554
+ "reactions": msg.get("reactions", [])
555
+ }
556
+ for msg in messages
557
+ ],
558
+ "count": len(messages),
559
+ "has_more": response.get("has_more", False)
560
+ }
561
+ return json.dumps(result, indent=2)
562
+
563
+ except Exception as e:
564
+ logger.error(f"Error getting channel history: {e}")
565
+ return json.dumps({
566
+ "success": False,
567
+ "error": str(e)
568
+ })
569
+
570
+ def search_messages(
571
+ self,
572
+ query: str,
573
+ count: int = 20,
574
+ sort: str = "score"
575
+ ) -> str:
576
+ """
577
+ Search for messages in Slack.
578
+
579
+ Args:
580
+ query: Search query (required)
581
+ count: Number of results (default: 20, max: 100)
582
+ sort: Sort by 'score' or 'timestamp' (default: score)
583
+
584
+ Returns:
585
+ JSON string with search results
586
+ """
587
+ try:
588
+ client = self._get_client()
589
+ response = client.search_messages(
590
+ query=query,
591
+ count=min(count, 100),
592
+ sort=sort
593
+ )
594
+
595
+ messages = response.get("messages", {}).get("matches", [])
596
+ result = {
597
+ "success": True,
598
+ "query": query,
599
+ "matches": [
600
+ {
601
+ "text": msg.get("text"),
602
+ "user": msg.get("user"),
603
+ "username": msg.get("username"),
604
+ "channel": msg.get("channel", {}).get("name"),
605
+ "channel_id": msg.get("channel", {}).get("id"),
606
+ "ts": msg.get("ts"),
607
+ "permalink": msg.get("permalink"),
608
+ "type": msg.get("type")
609
+ }
610
+ for msg in messages
611
+ ],
612
+ "count": len(messages),
613
+ "total": response.get("messages", {}).get("total", 0)
614
+ }
615
+ return json.dumps(result, indent=2)
616
+
617
+ except Exception as e:
618
+ logger.error(f"Error searching messages: {e}")
619
+ return json.dumps({
620
+ "success": False,
621
+ "error": str(e)
622
+ })
623
+
624
+ def get_thread_replies(
625
+ self,
626
+ channel_id: str,
627
+ thread_ts: str,
628
+ limit: int = 50
629
+ ) -> str:
630
+ """
631
+ Get replies from a Slack thread.
632
+
633
+ Args:
634
+ channel_id: Channel ID (required)
635
+ thread_ts: Thread timestamp (required)
636
+ limit: Number of replies (default: 50, max: 1000)
637
+
638
+ Returns:
639
+ JSON string with thread replies
640
+ """
641
+ try:
642
+ client = self._get_client()
643
+ response = client.conversations_replies(
644
+ channel=channel_id,
645
+ ts=thread_ts,
646
+ limit=min(limit, 1000)
647
+ )
648
+
649
+ messages = response.get("messages", [])
650
+ result = {
651
+ "success": True,
652
+ "channel_id": channel_id,
653
+ "thread_ts": thread_ts,
654
+ "messages": [
655
+ {
656
+ "ts": msg.get("ts"),
657
+ "user": msg.get("user"),
658
+ "text": msg.get("text"),
659
+ "type": msg.get("type"),
660
+ "reactions": msg.get("reactions", [])
661
+ }
662
+ for msg in messages
663
+ ],
664
+ "count": len(messages)
665
+ }
666
+ return json.dumps(result, indent=2)
667
+
668
+ except Exception as e:
669
+ logger.error(f"Error getting thread replies: {e}")
670
+ return json.dumps({
671
+ "success": False,
672
+ "error": str(e)
673
+ })
674
+
675
+ def get_user_info(
676
+ self,
677
+ user_id: str
678
+ ) -> str:
679
+ """
680
+ Get information about a Slack user.
681
+
682
+ Args:
683
+ user_id: User ID (required)
684
+
685
+ Returns:
686
+ JSON string with user information
687
+ """
688
+ try:
689
+ client = self._get_client()
690
+ response = client.users_info(user=user_id)
691
+
692
+ user = response.get("user", {})
693
+ result = {
694
+ "success": True,
695
+ "user": {
696
+ "id": user.get("id"),
697
+ "name": user.get("name"),
698
+ "real_name": user.get("real_name"),
699
+ "display_name": user.get("profile", {}).get("display_name"),
700
+ "email": user.get("profile", {}).get("email"),
701
+ "title": user.get("profile", {}).get("title"),
702
+ "is_bot": user.get("is_bot", False),
703
+ "is_admin": user.get("is_admin", False),
704
+ "is_owner": user.get("is_owner", False),
705
+ "tz": user.get("tz"),
706
+ "status_text": user.get("profile", {}).get("status_text", ""),
707
+ "status_emoji": user.get("profile", {}).get("status_emoji", "")
708
+ }
709
+ }
710
+ return json.dumps(result, indent=2)
711
+
712
+ except Exception as e:
713
+ logger.error(f"Error getting user info: {e}")
714
+ return json.dumps({
715
+ "success": False,
716
+ "error": str(e)
717
+ })
718
+
719
+ def list_users(
720
+ self,
721
+ limit: int = 100
722
+ ) -> str:
723
+ """
724
+ List users in the Slack workspace.
725
+
726
+ Args:
727
+ limit: Maximum users to return (default: 100, max: 1000)
728
+
729
+ Returns:
730
+ JSON string with user list
731
+ """
732
+ try:
733
+ client = self._get_client()
734
+ response = client.users_list(limit=min(limit, 1000))
735
+
736
+ members = response.get("members", [])
737
+ result = {
738
+ "success": True,
739
+ "users": [
740
+ {
741
+ "id": user.get("id"),
742
+ "name": user.get("name"),
743
+ "real_name": user.get("real_name"),
744
+ "display_name": user.get("profile", {}).get("display_name"),
745
+ "is_bot": user.get("is_bot", False),
746
+ "is_admin": user.get("is_admin", False),
747
+ "deleted": user.get("deleted", False)
748
+ }
749
+ for user in members
750
+ if not user.get("deleted", False) # Filter out deleted users
751
+ ],
752
+ "count": len([u for u in members if not u.get("deleted", False)])
753
+ }
754
+ return json.dumps(result, indent=2)
755
+
756
+ except Exception as e:
757
+ logger.error(f"Error listing users: {e}")
758
+ return json.dumps({
759
+ "success": False,
760
+ "error": str(e)
761
+ })
762
+
763
+ # ========================================================================
764
+ # Write Messages Group
765
+ # ========================================================================
766
+
767
+ def send_message(
768
+ self,
769
+ channel: str,
770
+ text: str,
771
+ thread_ts: Optional[str] = None,
772
+ blocks: Optional[str] = None
773
+ ) -> str:
774
+ """
775
+ Send a message to a Slack channel.
776
+
777
+ Args:
778
+ channel: Channel ID or name (required)
779
+ text: Message text (required)
780
+ thread_ts: Thread timestamp (to reply in thread)
781
+ blocks: Optional Block Kit blocks as JSON string
782
+
783
+ Returns:
784
+ JSON string with message details
785
+ """
786
+ try:
787
+ # Check channel scoping
788
+ is_allowed, error_msg = self._is_channel_allowed(channel, channel)
789
+ if not is_allowed:
790
+ return json.dumps({
791
+ "success": False,
792
+ "error": f"❌ Channel access denied: {error_msg}"
793
+ })
794
+
795
+ client = self._get_client()
796
+ kwargs = {
797
+ "channel": channel,
798
+ "text": text
799
+ }
800
+ if thread_ts:
801
+ kwargs["thread_ts"] = thread_ts
802
+ if blocks:
803
+ kwargs["blocks"] = json.loads(blocks) if isinstance(blocks, str) else blocks
804
+
805
+ response = client.chat_postMessage(**kwargs)
806
+
807
+ result = {
808
+ "success": True,
809
+ "channel": response.get("channel"),
810
+ "ts": response.get("ts"),
811
+ "message": {
812
+ "text": response.get("message", {}).get("text"),
813
+ "user": response.get("message", {}).get("user"),
814
+ "ts": response.get("message", {}).get("ts")
815
+ }
816
+ }
817
+ return json.dumps(result, indent=2)
818
+
819
+ except Exception as e:
820
+ logger.error(f"Error sending message: {e}")
821
+ return json.dumps({
822
+ "success": False,
823
+ "error": str(e)
824
+ })
825
+
826
+ def post_message(
827
+ self,
828
+ channel: str,
829
+ text: str,
830
+ blocks: Optional[str] = None
831
+ ) -> str:
832
+ """
833
+ Post a message to a Slack channel (alias for send_message without thread).
834
+
835
+ Args:
836
+ channel: Channel ID or name (required)
837
+ text: Message text (required)
838
+ blocks: Optional Block Kit blocks as JSON string
839
+
840
+ Returns:
841
+ JSON string with message details
842
+ """
843
+ return self.send_message(channel=channel, text=text, blocks=blocks)
844
+
845
+ def reply_to_thread(
846
+ self,
847
+ channel: str,
848
+ thread_ts: str,
849
+ text: str
850
+ ) -> str:
851
+ """
852
+ Reply to a Slack thread.
853
+
854
+ Args:
855
+ channel: Channel ID or name (required)
856
+ thread_ts: Thread timestamp (required)
857
+ text: Reply text (required)
858
+
859
+ Returns:
860
+ JSON string with reply details
861
+ """
862
+ return self.send_message(channel=channel, text=text, thread_ts=thread_ts)
863
+
864
+ def update_message(
865
+ self,
866
+ channel: str,
867
+ ts: str,
868
+ text: str,
869
+ blocks: Optional[str] = None
870
+ ) -> str:
871
+ """
872
+ Update an existing Slack message.
873
+
874
+ Args:
875
+ channel: Channel ID (required)
876
+ ts: Message timestamp (required)
877
+ text: New message text (required)
878
+ blocks: Optional Block Kit blocks as JSON string
879
+
880
+ Returns:
881
+ JSON string with update confirmation
882
+ """
883
+ try:
884
+ client = self._get_client()
885
+ kwargs = {
886
+ "channel": channel,
887
+ "ts": ts,
888
+ "text": text
889
+ }
890
+ if blocks:
891
+ kwargs["blocks"] = json.loads(blocks) if isinstance(blocks, str) else blocks
892
+
893
+ response = client.chat_update(**kwargs)
894
+
895
+ result = {
896
+ "success": True,
897
+ "channel": response.get("channel"),
898
+ "ts": response.get("ts"),
899
+ "text": response.get("text")
900
+ }
901
+ return json.dumps(result, indent=2)
902
+
903
+ except Exception as e:
904
+ logger.error(f"Error updating message: {e}")
905
+ return json.dumps({
906
+ "success": False,
907
+ "error": str(e)
908
+ })
909
+
910
+ def delete_message(
911
+ self,
912
+ channel: str,
913
+ ts: str
914
+ ) -> str:
915
+ """
916
+ Delete a Slack message.
917
+
918
+ Args:
919
+ channel: Channel ID (required)
920
+ ts: Message timestamp (required)
921
+
922
+ Returns:
923
+ JSON string with deletion confirmation
924
+ """
925
+ try:
926
+ client = self._get_client()
927
+ response = client.chat_delete(
928
+ channel=channel,
929
+ ts=ts
930
+ )
931
+
932
+ result = {
933
+ "success": True,
934
+ "channel": response.get("channel"),
935
+ "ts": response.get("ts")
936
+ }
937
+ return json.dumps(result, indent=2)
938
+
939
+ except Exception as e:
940
+ logger.error(f"Error deleting message: {e}")
941
+ return json.dumps({
942
+ "success": False,
943
+ "error": str(e)
944
+ })
945
+
946
+ # ========================================================================
947
+ # Reactions Group
948
+ # ========================================================================
949
+
950
+ def add_reaction(
951
+ self,
952
+ channel: str,
953
+ ts: str,
954
+ reaction: str
955
+ ) -> str:
956
+ """
957
+ Add a reaction to a Slack message.
958
+
959
+ Args:
960
+ channel: Channel ID (required)
961
+ ts: Message timestamp (required)
962
+ reaction: Reaction emoji name without colons (e.g., 'thumbsup')
963
+
964
+ Returns:
965
+ JSON string with confirmation
966
+ """
967
+ try:
968
+ client = self._get_client()
969
+ # Remove colons if present
970
+ reaction = reaction.strip(':')
971
+
972
+ response = client.reactions_add(
973
+ channel=channel,
974
+ timestamp=ts,
975
+ name=reaction
976
+ )
977
+
978
+ result = {
979
+ "success": True,
980
+ "reaction": reaction,
981
+ "channel": channel,
982
+ "ts": ts
983
+ }
984
+ return json.dumps(result, indent=2)
985
+
986
+ except Exception as e:
987
+ logger.error(f"Error adding reaction: {e}")
988
+ return json.dumps({
989
+ "success": False,
990
+ "error": str(e)
991
+ })
992
+
993
+ def remove_reaction(
994
+ self,
995
+ channel: str,
996
+ ts: str,
997
+ reaction: str
998
+ ) -> str:
999
+ """
1000
+ Remove a reaction from a Slack message.
1001
+
1002
+ Args:
1003
+ channel: Channel ID (required)
1004
+ ts: Message timestamp (required)
1005
+ reaction: Reaction emoji name without colons (e.g., 'thumbsup')
1006
+
1007
+ Returns:
1008
+ JSON string with confirmation
1009
+ """
1010
+ try:
1011
+ client = self._get_client()
1012
+ # Remove colons if present
1013
+ reaction = reaction.strip(':')
1014
+
1015
+ response = client.reactions_remove(
1016
+ channel=channel,
1017
+ timestamp=ts,
1018
+ name=reaction
1019
+ )
1020
+
1021
+ result = {
1022
+ "success": True,
1023
+ "reaction": reaction,
1024
+ "channel": channel,
1025
+ "ts": ts
1026
+ }
1027
+ return json.dumps(result, indent=2)
1028
+
1029
+ except Exception as e:
1030
+ logger.error(f"Error removing reaction: {e}")
1031
+ return json.dumps({
1032
+ "success": False,
1033
+ "error": str(e)
1034
+ })
1035
+
1036
+ def list_reactions(
1037
+ self,
1038
+ channel: str,
1039
+ ts: str
1040
+ ) -> str:
1041
+ """
1042
+ List reactions on a Slack message.
1043
+
1044
+ Args:
1045
+ channel: Channel ID (required)
1046
+ ts: Message timestamp (required)
1047
+
1048
+ Returns:
1049
+ JSON string with reactions list
1050
+ """
1051
+ try:
1052
+ client = self._get_client()
1053
+ response = client.reactions_get(
1054
+ channel=channel,
1055
+ timestamp=ts
1056
+ )
1057
+
1058
+ message = response.get("message", {})
1059
+ reactions = message.get("reactions", [])
1060
+
1061
+ result = {
1062
+ "success": True,
1063
+ "channel": channel,
1064
+ "ts": ts,
1065
+ "reactions": [
1066
+ {
1067
+ "name": r.get("name"),
1068
+ "count": r.get("count"),
1069
+ "users": r.get("users", [])
1070
+ }
1071
+ for r in reactions
1072
+ ],
1073
+ "count": len(reactions)
1074
+ }
1075
+ return json.dumps(result, indent=2)
1076
+
1077
+ except Exception as e:
1078
+ logger.error(f"Error listing reactions: {e}")
1079
+ return json.dumps({
1080
+ "success": False,
1081
+ "error": str(e)
1082
+ })
1083
+
1084
+ # ========================================================================
1085
+ # Channel Management Group
1086
+ # ========================================================================
1087
+
1088
+ def create_channel(
1089
+ self,
1090
+ name: str,
1091
+ is_private: bool = False,
1092
+ description: Optional[str] = None
1093
+ ) -> str:
1094
+ """
1095
+ Create a new Slack channel.
1096
+
1097
+ Args:
1098
+ name: Channel name (required, lowercase, no spaces)
1099
+ is_private: Create as private channel (default: False)
1100
+ description: Optional channel description
1101
+
1102
+ Returns:
1103
+ JSON string with new channel details
1104
+ """
1105
+ try:
1106
+ client = self._get_client()
1107
+ response = client.conversations_create(
1108
+ name=name,
1109
+ is_private=is_private
1110
+ )
1111
+
1112
+ channel = response.get("channel", {})
1113
+
1114
+ # Set description if provided
1115
+ if description and channel.get("id"):
1116
+ try:
1117
+ client.conversations_setPurpose(
1118
+ channel=channel["id"],
1119
+ purpose=description
1120
+ )
1121
+ except:
1122
+ pass # Description setting is optional
1123
+
1124
+ result = {
1125
+ "success": True,
1126
+ "channel": {
1127
+ "id": channel.get("id"),
1128
+ "name": channel.get("name"),
1129
+ "is_private": channel.get("is_private", False),
1130
+ "created": channel.get("created")
1131
+ }
1132
+ }
1133
+ return json.dumps(result, indent=2)
1134
+
1135
+ except Exception as e:
1136
+ logger.error(f"Error creating channel: {e}")
1137
+ return json.dumps({
1138
+ "success": False,
1139
+ "error": str(e)
1140
+ })
1141
+
1142
+ def invite_to_channel(
1143
+ self,
1144
+ channel: str,
1145
+ user_ids: str
1146
+ ) -> str:
1147
+ """
1148
+ Invite users to a Slack channel.
1149
+
1150
+ Args:
1151
+ channel: Channel ID (required)
1152
+ user_ids: Comma-separated user IDs (required)
1153
+
1154
+ Returns:
1155
+ JSON string with confirmation
1156
+ """
1157
+ try:
1158
+ client = self._get_client()
1159
+ # Parse user IDs
1160
+ users = [uid.strip() for uid in user_ids.split(',')]
1161
+
1162
+ response = client.conversations_invite(
1163
+ channel=channel,
1164
+ users=users
1165
+ )
1166
+
1167
+ result = {
1168
+ "success": True,
1169
+ "channel": response.get("channel", {}).get("id"),
1170
+ "invited_users": users,
1171
+ "count": len(users)
1172
+ }
1173
+ return json.dumps(result, indent=2)
1174
+
1175
+ except Exception as e:
1176
+ logger.error(f"Error inviting to channel: {e}")
1177
+ return json.dumps({
1178
+ "success": False,
1179
+ "error": str(e)
1180
+ })
1181
+
1182
+ def archive_channel(
1183
+ self,
1184
+ channel: str
1185
+ ) -> str:
1186
+ """
1187
+ Archive a Slack channel.
1188
+
1189
+ Args:
1190
+ channel: Channel ID (required)
1191
+
1192
+ Returns:
1193
+ JSON string with confirmation
1194
+ """
1195
+ try:
1196
+ client = self._get_client()
1197
+ response = client.conversations_archive(channel=channel)
1198
+
1199
+ result = {
1200
+ "success": True,
1201
+ "channel": channel
1202
+ }
1203
+ return json.dumps(result, indent=2)
1204
+
1205
+ except Exception as e:
1206
+ logger.error(f"Error archiving channel: {e}")
1207
+ return json.dumps({
1208
+ "success": False,
1209
+ "error": str(e)
1210
+ })
1211
+
1212
+ def set_channel_topic(
1213
+ self,
1214
+ channel: str,
1215
+ topic: str
1216
+ ) -> str:
1217
+ """
1218
+ Set a Slack channel's topic.
1219
+
1220
+ Args:
1221
+ channel: Channel ID (required)
1222
+ topic: New topic text (required)
1223
+
1224
+ Returns:
1225
+ JSON string with confirmation
1226
+ """
1227
+ try:
1228
+ client = self._get_client()
1229
+ response = client.conversations_setTopic(
1230
+ channel=channel,
1231
+ topic=topic
1232
+ )
1233
+
1234
+ result = {
1235
+ "success": True,
1236
+ "channel": response.get("channel"),
1237
+ "topic": response.get("topic")
1238
+ }
1239
+ return json.dumps(result, indent=2)
1240
+
1241
+ except Exception as e:
1242
+ logger.error(f"Error setting channel topic: {e}")
1243
+ return json.dumps({
1244
+ "success": False,
1245
+ "error": str(e)
1246
+ })
1247
+
1248
+ def set_channel_description(
1249
+ self,
1250
+ channel: str,
1251
+ description: str
1252
+ ) -> str:
1253
+ """
1254
+ Set a Slack channel's description (purpose).
1255
+
1256
+ Args:
1257
+ channel: Channel ID (required)
1258
+ description: New description text (required)
1259
+
1260
+ Returns:
1261
+ JSON string with confirmation
1262
+ """
1263
+ try:
1264
+ client = self._get_client()
1265
+ response = client.conversations_setPurpose(
1266
+ channel=channel,
1267
+ purpose=description
1268
+ )
1269
+
1270
+ result = {
1271
+ "success": True,
1272
+ "channel": response.get("channel"),
1273
+ "purpose": response.get("purpose")
1274
+ }
1275
+ return json.dumps(result, indent=2)
1276
+
1277
+ except Exception as e:
1278
+ logger.error(f"Error setting channel description: {e}")
1279
+ return json.dumps({
1280
+ "success": False,
1281
+ "error": str(e)
1282
+ })