gobby 0.2.9__py3-none-any.whl → 0.2.11__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 (134) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/__init__.py +6 -0
  3. gobby/adapters/base.py +11 -2
  4. gobby/adapters/claude_code.py +2 -2
  5. gobby/adapters/codex_impl/adapter.py +38 -43
  6. gobby/adapters/copilot.py +324 -0
  7. gobby/adapters/cursor.py +373 -0
  8. gobby/adapters/gemini.py +2 -26
  9. gobby/adapters/windsurf.py +359 -0
  10. gobby/agents/definitions.py +162 -2
  11. gobby/agents/isolation.py +33 -1
  12. gobby/agents/pty_reader.py +192 -0
  13. gobby/agents/registry.py +10 -1
  14. gobby/agents/runner.py +24 -8
  15. gobby/agents/sandbox.py +8 -3
  16. gobby/agents/session.py +4 -0
  17. gobby/agents/spawn.py +9 -2
  18. gobby/agents/spawn_executor.py +49 -61
  19. gobby/agents/spawners/command_builder.py +4 -4
  20. gobby/app_context.py +5 -0
  21. gobby/cli/__init__.py +4 -0
  22. gobby/cli/install.py +259 -4
  23. gobby/cli/installers/__init__.py +12 -0
  24. gobby/cli/installers/copilot.py +242 -0
  25. gobby/cli/installers/cursor.py +244 -0
  26. gobby/cli/installers/shared.py +3 -0
  27. gobby/cli/installers/windsurf.py +242 -0
  28. gobby/cli/pipelines.py +639 -0
  29. gobby/cli/sessions.py +3 -1
  30. gobby/cli/skills.py +209 -0
  31. gobby/cli/tasks/crud.py +6 -5
  32. gobby/cli/tasks/search.py +1 -1
  33. gobby/cli/ui.py +116 -0
  34. gobby/cli/workflows.py +38 -17
  35. gobby/config/app.py +5 -0
  36. gobby/config/skills.py +23 -2
  37. gobby/hooks/broadcaster.py +9 -0
  38. gobby/hooks/event_handlers/_base.py +6 -1
  39. gobby/hooks/event_handlers/_session.py +44 -130
  40. gobby/hooks/events.py +48 -0
  41. gobby/hooks/hook_manager.py +25 -3
  42. gobby/install/copilot/hooks/hook_dispatcher.py +203 -0
  43. gobby/install/cursor/hooks/hook_dispatcher.py +203 -0
  44. gobby/install/gemini/hooks/hook_dispatcher.py +8 -0
  45. gobby/install/windsurf/hooks/hook_dispatcher.py +205 -0
  46. gobby/llm/__init__.py +14 -1
  47. gobby/llm/claude.py +217 -1
  48. gobby/llm/service.py +149 -0
  49. gobby/mcp_proxy/instructions.py +9 -27
  50. gobby/mcp_proxy/models.py +1 -0
  51. gobby/mcp_proxy/registries.py +56 -9
  52. gobby/mcp_proxy/server.py +6 -2
  53. gobby/mcp_proxy/services/tool_filter.py +7 -0
  54. gobby/mcp_proxy/services/tool_proxy.py +19 -1
  55. gobby/mcp_proxy/stdio.py +37 -21
  56. gobby/mcp_proxy/tools/agents.py +7 -0
  57. gobby/mcp_proxy/tools/hub.py +30 -1
  58. gobby/mcp_proxy/tools/orchestration/cleanup.py +5 -5
  59. gobby/mcp_proxy/tools/orchestration/monitor.py +1 -1
  60. gobby/mcp_proxy/tools/orchestration/orchestrate.py +8 -3
  61. gobby/mcp_proxy/tools/orchestration/review.py +17 -4
  62. gobby/mcp_proxy/tools/orchestration/wait.py +7 -7
  63. gobby/mcp_proxy/tools/pipelines/__init__.py +254 -0
  64. gobby/mcp_proxy/tools/pipelines/_discovery.py +67 -0
  65. gobby/mcp_proxy/tools/pipelines/_execution.py +281 -0
  66. gobby/mcp_proxy/tools/sessions/_crud.py +4 -4
  67. gobby/mcp_proxy/tools/sessions/_handoff.py +1 -1
  68. gobby/mcp_proxy/tools/skills/__init__.py +184 -30
  69. gobby/mcp_proxy/tools/spawn_agent.py +229 -14
  70. gobby/mcp_proxy/tools/tasks/_context.py +8 -0
  71. gobby/mcp_proxy/tools/tasks/_crud.py +27 -1
  72. gobby/mcp_proxy/tools/tasks/_helpers.py +1 -1
  73. gobby/mcp_proxy/tools/tasks/_lifecycle.py +125 -8
  74. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +2 -1
  75. gobby/mcp_proxy/tools/tasks/_search.py +1 -1
  76. gobby/mcp_proxy/tools/workflows/__init__.py +9 -2
  77. gobby/mcp_proxy/tools/workflows/_lifecycle.py +12 -1
  78. gobby/mcp_proxy/tools/workflows/_query.py +45 -26
  79. gobby/mcp_proxy/tools/workflows/_terminal.py +39 -3
  80. gobby/mcp_proxy/tools/worktrees.py +54 -15
  81. gobby/memory/context.py +5 -5
  82. gobby/runner.py +108 -6
  83. gobby/servers/http.py +7 -1
  84. gobby/servers/routes/__init__.py +2 -0
  85. gobby/servers/routes/admin.py +44 -0
  86. gobby/servers/routes/mcp/endpoints/execution.py +18 -25
  87. gobby/servers/routes/mcp/hooks.py +10 -1
  88. gobby/servers/routes/pipelines.py +227 -0
  89. gobby/servers/websocket.py +314 -1
  90. gobby/sessions/analyzer.py +87 -1
  91. gobby/sessions/manager.py +5 -5
  92. gobby/sessions/transcripts/__init__.py +3 -0
  93. gobby/sessions/transcripts/claude.py +5 -0
  94. gobby/sessions/transcripts/codex.py +5 -0
  95. gobby/sessions/transcripts/gemini.py +5 -0
  96. gobby/skills/hubs/__init__.py +25 -0
  97. gobby/skills/hubs/base.py +234 -0
  98. gobby/skills/hubs/claude_plugins.py +328 -0
  99. gobby/skills/hubs/clawdhub.py +289 -0
  100. gobby/skills/hubs/github_collection.py +465 -0
  101. gobby/skills/hubs/manager.py +263 -0
  102. gobby/skills/hubs/skillhub.py +342 -0
  103. gobby/storage/memories.py +4 -4
  104. gobby/storage/migrations.py +95 -3
  105. gobby/storage/pipelines.py +367 -0
  106. gobby/storage/sessions.py +23 -4
  107. gobby/storage/skills.py +1 -1
  108. gobby/storage/tasks/_aggregates.py +2 -2
  109. gobby/storage/tasks/_lifecycle.py +4 -4
  110. gobby/storage/tasks/_models.py +7 -1
  111. gobby/storage/tasks/_queries.py +3 -3
  112. gobby/sync/memories.py +4 -3
  113. gobby/tasks/commits.py +48 -17
  114. gobby/workflows/actions.py +75 -0
  115. gobby/workflows/context_actions.py +246 -5
  116. gobby/workflows/definitions.py +119 -1
  117. gobby/workflows/detection_helpers.py +23 -11
  118. gobby/workflows/enforcement/task_policy.py +18 -0
  119. gobby/workflows/engine.py +20 -1
  120. gobby/workflows/evaluator.py +8 -5
  121. gobby/workflows/lifecycle_evaluator.py +57 -26
  122. gobby/workflows/loader.py +567 -30
  123. gobby/workflows/lobster_compat.py +147 -0
  124. gobby/workflows/pipeline_executor.py +801 -0
  125. gobby/workflows/pipeline_state.py +172 -0
  126. gobby/workflows/pipeline_webhooks.py +206 -0
  127. gobby/workflows/premature_stop.py +5 -0
  128. gobby/worktrees/git.py +135 -20
  129. {gobby-0.2.9.dist-info → gobby-0.2.11.dist-info}/METADATA +56 -22
  130. {gobby-0.2.9.dist-info → gobby-0.2.11.dist-info}/RECORD +134 -106
  131. {gobby-0.2.9.dist-info → gobby-0.2.11.dist-info}/WHEEL +0 -0
  132. {gobby-0.2.9.dist-info → gobby-0.2.11.dist-info}/entry_points.txt +0 -0
  133. {gobby-0.2.9.dist-info → gobby-0.2.11.dist-info}/licenses/LICENSE.md +0 -0
  134. {gobby-0.2.9.dist-info → gobby-0.2.11.dist-info}/top_level.txt +0 -0
gobby/servers/http.py CHANGED
@@ -121,12 +121,13 @@ class HTTPServer:
121
121
  )
122
122
  logger.debug("Merge resolution and inter-session messaging subsystems initialized")
123
123
 
124
- # Setup internal registries (gobby-tasks, gobby-memory, etc.)
124
+ # Setup internal registries (gobby-tasks, gobby-memory, gobby-pipelines, etc.)
125
125
  self._internal_manager = setup_internal_registries(
126
126
  _config=services.config,
127
127
  _session_manager=None, # Not needed for internal registries
128
128
  memory_manager=services.memory_manager,
129
129
  task_manager=services.task_manager,
130
+ db=services.mcp_db_manager.db if services.mcp_db_manager else None,
130
131
  sync_manager=services.task_sync_manager,
131
132
  task_validator=services.task_validator,
132
133
  message_manager=services.message_manager,
@@ -142,6 +143,9 @@ class HTTPServer:
142
143
  project_id=services.project_id,
143
144
  tool_proxy_getter=tool_proxy_getter,
144
145
  inter_session_message_manager=inter_session_message_manager,
146
+ pipeline_executor=services.pipeline_executor,
147
+ workflow_loader=services.workflow_loader,
148
+ pipeline_execution_manager=services.pipeline_execution_manager,
145
149
  )
146
150
  registry_count = len(self._internal_manager)
147
151
  logger.debug(f"Internal registries initialized: {registry_count} registries")
@@ -514,6 +518,7 @@ class HTTPServer:
514
518
  create_admin_router,
515
519
  create_hooks_router,
516
520
  create_mcp_router,
521
+ create_pipelines_router,
517
522
  create_plugins_router,
518
523
  create_sessions_router,
519
524
  create_webhooks_router,
@@ -526,6 +531,7 @@ class HTTPServer:
526
531
  app.include_router(create_hooks_router(self))
527
532
  app.include_router(create_plugins_router())
528
533
  app.include_router(create_webhooks_router())
534
+ app.include_router(create_pipelines_router(self))
529
535
 
530
536
  async def _process_shutdown(self) -> None:
531
537
  """
@@ -11,12 +11,14 @@ from gobby.servers.routes.mcp import (
11
11
  create_plugins_router,
12
12
  create_webhooks_router,
13
13
  )
14
+ from gobby.servers.routes.pipelines import create_pipelines_router
14
15
  from gobby.servers.routes.sessions import create_sessions_router
15
16
 
16
17
  __all__ = [
17
18
  "create_admin_router",
18
19
  "create_hooks_router",
19
20
  "create_mcp_router",
21
+ "create_pipelines_router",
20
22
  "create_plugins_router",
21
23
  "create_sessions_router",
22
24
  "create_webhooks_router",
@@ -290,6 +290,50 @@ def create_admin_router(server: "HTTPServer") -> APIRouter:
290
290
  logger.error(f"Failed to export metrics: {e}", exc_info=True)
291
291
  raise
292
292
 
293
+ @router.get("/models")
294
+ async def get_models() -> dict[str, Any]:
295
+ """
296
+ Get available LLM providers and their models.
297
+
298
+ Returns:
299
+ Dictionary with providers and their available models
300
+ """
301
+ start_time = time.perf_counter()
302
+
303
+ providers_data: dict[str, Any] = {}
304
+ default_provider = None
305
+ default_model = None
306
+
307
+ if server.llm_service is not None:
308
+ enabled = server.llm_service.enabled_providers
309
+ if enabled:
310
+ default_provider = "claude" if "claude" in enabled else enabled[0]
311
+
312
+ # Get models for each enabled provider from config
313
+ if server.services.config and server.services.config.llm_providers:
314
+ llm_config = server.services.config.llm_providers
315
+
316
+ for provider_name in enabled:
317
+ provider_config = getattr(llm_config, provider_name, None)
318
+ if provider_config:
319
+ models = provider_config.get_models_list()
320
+ providers_data[provider_name] = {
321
+ "models": models,
322
+ "auth_mode": provider_config.auth_mode,
323
+ }
324
+ # Set default model from first provider
325
+ if default_model is None and models:
326
+ default_model = models[0]
327
+
328
+ response_time_ms = (time.perf_counter() - start_time) * 1000
329
+
330
+ return {
331
+ "providers": providers_data,
332
+ "default_provider": default_provider,
333
+ "default_model": default_model,
334
+ "response_time_ms": response_time_ms,
335
+ }
336
+
293
337
  @router.get("/config")
294
338
  async def get_config() -> dict[str, Any]:
295
339
  """
@@ -7,7 +7,6 @@ These endpoints handle tool listing, schema retrieval, and tool execution.
7
7
 
8
8
  import json
9
9
  import logging
10
- import re
11
10
  import time
12
11
  from typing import TYPE_CHECKING, Any
13
12
 
@@ -36,6 +35,10 @@ def _process_tool_proxy_result(
36
35
  """
37
36
  Process tool proxy result with consistent metrics, logging, and error handling.
38
37
 
38
+ All errors (including server-not-found) are returned as {success: False, result: {...}}
39
+ rather than raising HTTPException. This ensures consistent error formatting for
40
+ MCP clients, which otherwise show errors as "HTTP 404: {body}" instead of clean messages.
41
+
39
42
  Args:
40
43
  result: The result from tool_proxy.call_tool()
41
44
  server_name: Name of the MCP server
@@ -44,38 +47,28 @@ def _process_tool_proxy_result(
44
47
 
45
48
  Returns:
46
49
  Wrapped result dict with success status and response time
47
-
48
- Raises:
49
- HTTPException: 404 if server not found/not configured
50
50
  """
51
51
  # Track metrics for tool-level failures vs successes
52
52
  if isinstance(result, dict) and result.get("success") is False:
53
53
  _metrics.inc_counter("mcp_tool_calls_failed_total")
54
54
 
55
- # Check structured error code first (preferred)
55
+ # Return all errors consistently as {success: False, result: {...}}
56
+ # Previously, server-not-found errors raised HTTPException(404), but this
57
+ # caused MCP clients to format errors as "HTTP 404: {body}" instead of
58
+ # showing clean error messages. Consistent error envelopes work better
59
+ # for both MCP clients (via call_tool) and HTTP clients.
56
60
  error_code = result.get("error_code")
57
- if error_code in ("SERVER_NOT_FOUND", "SERVER_NOT_CONFIGURED"):
58
- # Normalize result to standard error shape while preserving existing fields
59
- normalized = {"success": False, "error": result.get("error", "Unknown error")}
60
- for key, value in result.items():
61
- if key not in normalized:
62
- normalized[key] = value
63
- raise HTTPException(status_code=404, detail=normalized)
64
-
65
- # Backward compatibility: fall back to regex matching if no error_code
66
- if not error_code:
61
+ if error_code:
67
62
  logger.debug(
68
- "ToolProxyService returned error without error_code - using regex fallback"
63
+ f"MCP tool call failed: {server_name}.{tool_name} (error_code={error_code})",
64
+ extra={
65
+ "server": server_name,
66
+ "tool": tool_name,
67
+ "error_code": error_code,
68
+ },
69
69
  )
70
- error_msg = str(result.get("error", ""))
71
- if re.search(r"server\s+(not\s+found|not\s+configured)", error_msg, re.IGNORECASE):
72
- normalized = {"success": False, "error": result.get("error", "Unknown error")}
73
- for key, value in result.items():
74
- if key not in normalized:
75
- normalized[key] = value
76
- raise HTTPException(status_code=404, detail=normalized)
77
-
78
- # Tool-level failure (not a transport error) - return failure envelope
70
+
71
+ # Tool-level failure - return failure envelope (not HTTP exception)
79
72
  return {
80
73
  "success": False,
81
74
  "result": result,
@@ -113,12 +113,21 @@ def create_hooks_router(server: "HTTPServer") -> APIRouter:
113
113
  from gobby.adapters.base import BaseAdapter
114
114
  from gobby.adapters.claude_code import ClaudeCodeAdapter
115
115
  from gobby.adapters.codex_impl.adapter import CodexNotifyAdapter
116
+ from gobby.adapters.copilot import CopilotAdapter
117
+ from gobby.adapters.cursor import CursorAdapter
116
118
  from gobby.adapters.gemini import GeminiAdapter
119
+ from gobby.adapters.windsurf import WindsurfAdapter
117
120
 
118
121
  if source == "claude":
119
122
  adapter: BaseAdapter = ClaudeCodeAdapter(hook_manager=hook_manager)
120
123
  elif source == "antigravity":
121
124
  adapter = ClaudeCodeAdapter(hook_manager=hook_manager) # Same format as Claude
125
+ elif source == "cursor":
126
+ adapter = CursorAdapter(hook_manager=hook_manager)
127
+ elif source == "windsurf":
128
+ adapter = WindsurfAdapter(hook_manager=hook_manager)
129
+ elif source == "copilot":
130
+ adapter = CopilotAdapter(hook_manager=hook_manager)
122
131
  elif source == "gemini":
123
132
  adapter = GeminiAdapter(hook_manager=hook_manager)
124
133
  elif source == "codex":
@@ -126,7 +135,7 @@ def create_hooks_router(server: "HTTPServer") -> APIRouter:
126
135
  else:
127
136
  raise HTTPException(
128
137
  status_code=400,
129
- detail=f"Unsupported source: {source}. Supported: claude, antigravity, gemini, codex",
138
+ detail=f"Unsupported source: {source}. Supported: claude, antigravity, gemini, codex, cursor, windsurf, copilot",
130
139
  )
131
140
 
132
141
  # Execute hook via adapter
@@ -0,0 +1,227 @@
1
+ """
2
+ Pipeline routes for Gobby HTTP server.
3
+
4
+ Provides endpoints for running, approving, and monitoring pipelines.
5
+ """
6
+
7
+ import logging
8
+ from typing import TYPE_CHECKING, Any
9
+
10
+ from fastapi import APIRouter, HTTPException
11
+ from fastapi.responses import JSONResponse
12
+ from pydantic import BaseModel
13
+
14
+ if TYPE_CHECKING:
15
+ from gobby.servers.http import HTTPServer
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class PipelineRunRequest(BaseModel):
21
+ """Request body for POST /api/pipelines/run."""
22
+
23
+ name: str
24
+ inputs: dict[str, Any] = {}
25
+ project_id: str | None = None
26
+
27
+
28
+ class PipelineRunResponse(BaseModel):
29
+ """Response body for successful pipeline execution."""
30
+
31
+ status: str
32
+ execution_id: str
33
+ pipeline_name: str
34
+ outputs: dict[str, Any] | None = None
35
+
36
+
37
+ class PipelineApprovalResponse(BaseModel):
38
+ """Response body when pipeline requires approval."""
39
+
40
+ status: str
41
+ execution_id: str
42
+ step_id: str
43
+ token: str
44
+ message: str
45
+
46
+
47
+ def create_pipelines_router(server: "HTTPServer") -> APIRouter:
48
+ """
49
+ Create pipelines router with endpoints bound to server instance.
50
+
51
+ Args:
52
+ server: HTTPServer instance for accessing state and dependencies
53
+
54
+ Returns:
55
+ Configured APIRouter with pipeline endpoints
56
+ """
57
+ router = APIRouter(prefix="/api/pipelines", tags=["pipelines"])
58
+
59
+ @router.post("/run", response_model=None)
60
+ async def run_pipeline(request: PipelineRunRequest) -> dict[str, Any] | JSONResponse:
61
+ """
62
+ Run a pipeline by name.
63
+
64
+ Returns:
65
+ 200: Pipeline completed successfully
66
+ 202: Pipeline waiting for approval
67
+ 404: Pipeline not found
68
+ 500: Execution error
69
+ """
70
+ from gobby.workflows.pipeline_state import ApprovalRequired
71
+
72
+ # Get loader and executor from services
73
+ loader = server.services.workflow_loader
74
+ executor = server.services.pipeline_executor
75
+
76
+ if loader is None:
77
+ raise HTTPException(status_code=500, detail="Workflow loader not configured")
78
+
79
+ if executor is None:
80
+ raise HTTPException(status_code=500, detail="Pipeline executor not configured")
81
+
82
+ # Load the pipeline
83
+ pipeline = loader.load_pipeline(request.name)
84
+ if pipeline is None:
85
+ raise HTTPException(status_code=404, detail=f"Pipeline '{request.name}' not found")
86
+
87
+ try:
88
+ # Execute the pipeline
89
+ execution = await executor.execute(
90
+ pipeline=pipeline,
91
+ inputs=request.inputs,
92
+ project_id=request.project_id or "",
93
+ )
94
+
95
+ # Return success response
96
+ return {
97
+ "status": execution.status.value,
98
+ "execution_id": execution.id,
99
+ "pipeline_name": execution.pipeline_name,
100
+ }
101
+
102
+ except ApprovalRequired as e:
103
+ # Return 202 Accepted for approval required
104
+ return JSONResponse(
105
+ status_code=202,
106
+ content={
107
+ "status": "waiting_approval",
108
+ "execution_id": e.execution_id,
109
+ "step_id": e.step_id,
110
+ "token": e.token,
111
+ "message": e.message,
112
+ },
113
+ )
114
+
115
+ except Exception as e:
116
+ logger.error(f"Pipeline execution failed: {e}", exc_info=True)
117
+ raise HTTPException(status_code=500, detail=f"Execution error: {e}") from None
118
+
119
+ @router.get("/{execution_id}")
120
+ async def get_execution(execution_id: str) -> dict[str, Any]:
121
+ """
122
+ Get execution details by ID.
123
+
124
+ Returns:
125
+ 200: Execution details with steps
126
+ 404: Execution not found
127
+ """
128
+ # Get execution manager from services
129
+ execution_manager = server.services.pipeline_execution_manager
130
+
131
+ if execution_manager is None:
132
+ raise HTTPException(status_code=500, detail="Pipeline execution manager not configured")
133
+
134
+ # Fetch execution
135
+ execution = execution_manager.get_execution(execution_id)
136
+ if execution is None:
137
+ raise HTTPException(status_code=404, detail=f"Execution '{execution_id}' not found")
138
+
139
+ # Fetch steps
140
+ steps = execution_manager.get_steps_for_execution(execution_id)
141
+
142
+ return {
143
+ "id": execution.id,
144
+ "pipeline_name": execution.pipeline_name,
145
+ "project_id": execution.project_id,
146
+ "status": execution.status.value,
147
+ "created_at": execution.created_at,
148
+ "updated_at": execution.updated_at,
149
+ "steps": [
150
+ {
151
+ "id": step.id,
152
+ "step_id": step.step_id,
153
+ "status": step.status.value,
154
+ }
155
+ for step in steps
156
+ ],
157
+ }
158
+
159
+ @router.post("/approve/{token}", response_model=None)
160
+ async def approve_execution(token: str) -> dict[str, Any] | JSONResponse:
161
+ """
162
+ Approve a pipeline execution waiting for approval.
163
+
164
+ Returns:
165
+ 200: Execution resumed and completed (or continued)
166
+ 202: Execution resumed but needs another approval
167
+ 404: Invalid token
168
+ """
169
+ from gobby.workflows.pipeline_state import ApprovalRequired
170
+
171
+ executor = server.services.pipeline_executor
172
+
173
+ if executor is None:
174
+ raise HTTPException(status_code=500, detail="Pipeline executor not configured")
175
+
176
+ try:
177
+ execution = await executor.approve(token, approved_by=None)
178
+
179
+ return {
180
+ "status": execution.status.value,
181
+ "execution_id": execution.id,
182
+ "pipeline_name": execution.pipeline_name,
183
+ }
184
+
185
+ except ApprovalRequired as e:
186
+ # Pipeline needs another approval
187
+ return JSONResponse(
188
+ status_code=202,
189
+ content={
190
+ "status": "waiting_approval",
191
+ "execution_id": e.execution_id,
192
+ "step_id": e.step_id,
193
+ "token": e.token,
194
+ "message": e.message,
195
+ },
196
+ )
197
+
198
+ except ValueError as e:
199
+ raise HTTPException(status_code=404, detail=f"Invalid token: {e}") from None
200
+
201
+ @router.post("/reject/{token}")
202
+ async def reject_execution(token: str) -> dict[str, Any]:
203
+ """
204
+ Reject a pipeline execution waiting for approval.
205
+
206
+ Returns:
207
+ 200: Execution rejected/cancelled
208
+ 404: Invalid token
209
+ """
210
+ executor = server.services.pipeline_executor
211
+
212
+ if executor is None:
213
+ raise HTTPException(status_code=500, detail="Pipeline executor not configured")
214
+
215
+ try:
216
+ execution = await executor.reject(token, rejected_by=None)
217
+
218
+ return {
219
+ "status": execution.status.value,
220
+ "execution_id": execution.id,
221
+ "pipeline_name": execution.pipeline_name,
222
+ }
223
+
224
+ except ValueError as e:
225
+ raise HTTPException(status_code=404, detail=f"Invalid token: {e}") from None
226
+
227
+ return router