gobby 0.2.8__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 (168) 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 +5 -28
  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 +64 -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/utils.py +5 -17
  35. gobby/cli/workflows.py +38 -17
  36. gobby/config/app.py +5 -0
  37. gobby/config/features.py +0 -20
  38. gobby/config/skills.py +23 -2
  39. gobby/config/tasks.py +4 -0
  40. gobby/hooks/broadcaster.py +9 -0
  41. gobby/hooks/event_handlers/__init__.py +155 -0
  42. gobby/hooks/event_handlers/_agent.py +175 -0
  43. gobby/hooks/event_handlers/_base.py +92 -0
  44. gobby/hooks/event_handlers/_misc.py +66 -0
  45. gobby/hooks/event_handlers/_session.py +487 -0
  46. gobby/hooks/event_handlers/_tool.py +196 -0
  47. gobby/hooks/events.py +48 -0
  48. gobby/hooks/hook_manager.py +27 -3
  49. gobby/install/copilot/hooks/hook_dispatcher.py +203 -0
  50. gobby/install/cursor/hooks/hook_dispatcher.py +203 -0
  51. gobby/install/gemini/hooks/hook_dispatcher.py +8 -0
  52. gobby/install/windsurf/hooks/hook_dispatcher.py +205 -0
  53. gobby/llm/__init__.py +14 -1
  54. gobby/llm/claude.py +594 -43
  55. gobby/llm/service.py +149 -0
  56. gobby/mcp_proxy/importer.py +4 -41
  57. gobby/mcp_proxy/instructions.py +9 -27
  58. gobby/mcp_proxy/manager.py +13 -3
  59. gobby/mcp_proxy/models.py +1 -0
  60. gobby/mcp_proxy/registries.py +66 -5
  61. gobby/mcp_proxy/server.py +6 -2
  62. gobby/mcp_proxy/services/recommendation.py +2 -28
  63. gobby/mcp_proxy/services/tool_filter.py +7 -0
  64. gobby/mcp_proxy/services/tool_proxy.py +19 -1
  65. gobby/mcp_proxy/stdio.py +37 -21
  66. gobby/mcp_proxy/tools/agents.py +7 -0
  67. gobby/mcp_proxy/tools/artifacts.py +3 -3
  68. gobby/mcp_proxy/tools/hub.py +30 -1
  69. gobby/mcp_proxy/tools/orchestration/cleanup.py +5 -5
  70. gobby/mcp_proxy/tools/orchestration/monitor.py +1 -1
  71. gobby/mcp_proxy/tools/orchestration/orchestrate.py +8 -3
  72. gobby/mcp_proxy/tools/orchestration/review.py +17 -4
  73. gobby/mcp_proxy/tools/orchestration/wait.py +7 -7
  74. gobby/mcp_proxy/tools/pipelines/__init__.py +254 -0
  75. gobby/mcp_proxy/tools/pipelines/_discovery.py +67 -0
  76. gobby/mcp_proxy/tools/pipelines/_execution.py +281 -0
  77. gobby/mcp_proxy/tools/sessions/_crud.py +4 -4
  78. gobby/mcp_proxy/tools/sessions/_handoff.py +1 -1
  79. gobby/mcp_proxy/tools/skills/__init__.py +184 -30
  80. gobby/mcp_proxy/tools/spawn_agent.py +229 -14
  81. gobby/mcp_proxy/tools/task_readiness.py +27 -4
  82. gobby/mcp_proxy/tools/tasks/_context.py +8 -0
  83. gobby/mcp_proxy/tools/tasks/_crud.py +27 -1
  84. gobby/mcp_proxy/tools/tasks/_helpers.py +1 -1
  85. gobby/mcp_proxy/tools/tasks/_lifecycle.py +125 -8
  86. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +2 -1
  87. gobby/mcp_proxy/tools/tasks/_search.py +1 -1
  88. gobby/mcp_proxy/tools/workflows/__init__.py +273 -0
  89. gobby/mcp_proxy/tools/workflows/_artifacts.py +225 -0
  90. gobby/mcp_proxy/tools/workflows/_import.py +112 -0
  91. gobby/mcp_proxy/tools/workflows/_lifecycle.py +332 -0
  92. gobby/mcp_proxy/tools/workflows/_query.py +226 -0
  93. gobby/mcp_proxy/tools/workflows/_resolution.py +78 -0
  94. gobby/mcp_proxy/tools/workflows/_terminal.py +175 -0
  95. gobby/mcp_proxy/tools/worktrees.py +54 -15
  96. gobby/memory/components/__init__.py +0 -0
  97. gobby/memory/components/ingestion.py +98 -0
  98. gobby/memory/components/search.py +108 -0
  99. gobby/memory/context.py +5 -5
  100. gobby/memory/manager.py +16 -25
  101. gobby/paths.py +51 -0
  102. gobby/prompts/loader.py +1 -35
  103. gobby/runner.py +131 -16
  104. gobby/servers/http.py +193 -150
  105. gobby/servers/routes/__init__.py +2 -0
  106. gobby/servers/routes/admin.py +56 -0
  107. gobby/servers/routes/mcp/endpoints/execution.py +33 -32
  108. gobby/servers/routes/mcp/endpoints/registry.py +8 -8
  109. gobby/servers/routes/mcp/hooks.py +10 -1
  110. gobby/servers/routes/pipelines.py +227 -0
  111. gobby/servers/websocket.py +314 -1
  112. gobby/sessions/analyzer.py +89 -3
  113. gobby/sessions/manager.py +5 -5
  114. gobby/sessions/transcripts/__init__.py +3 -0
  115. gobby/sessions/transcripts/claude.py +5 -0
  116. gobby/sessions/transcripts/codex.py +5 -0
  117. gobby/sessions/transcripts/gemini.py +5 -0
  118. gobby/skills/hubs/__init__.py +25 -0
  119. gobby/skills/hubs/base.py +234 -0
  120. gobby/skills/hubs/claude_plugins.py +328 -0
  121. gobby/skills/hubs/clawdhub.py +289 -0
  122. gobby/skills/hubs/github_collection.py +465 -0
  123. gobby/skills/hubs/manager.py +263 -0
  124. gobby/skills/hubs/skillhub.py +342 -0
  125. gobby/skills/parser.py +23 -0
  126. gobby/skills/sync.py +5 -4
  127. gobby/storage/artifacts.py +19 -0
  128. gobby/storage/memories.py +4 -4
  129. gobby/storage/migrations.py +118 -3
  130. gobby/storage/pipelines.py +367 -0
  131. gobby/storage/sessions.py +23 -4
  132. gobby/storage/skills.py +48 -8
  133. gobby/storage/tasks/_aggregates.py +2 -2
  134. gobby/storage/tasks/_lifecycle.py +4 -4
  135. gobby/storage/tasks/_models.py +7 -1
  136. gobby/storage/tasks/_queries.py +3 -3
  137. gobby/sync/memories.py +4 -3
  138. gobby/tasks/commits.py +48 -17
  139. gobby/tasks/external_validator.py +4 -17
  140. gobby/tasks/validation.py +13 -87
  141. gobby/tools/summarizer.py +18 -51
  142. gobby/utils/status.py +13 -0
  143. gobby/workflows/actions.py +80 -0
  144. gobby/workflows/context_actions.py +265 -27
  145. gobby/workflows/definitions.py +119 -1
  146. gobby/workflows/detection_helpers.py +23 -11
  147. gobby/workflows/enforcement/__init__.py +11 -1
  148. gobby/workflows/enforcement/blocking.py +96 -0
  149. gobby/workflows/enforcement/handlers.py +35 -1
  150. gobby/workflows/enforcement/task_policy.py +18 -0
  151. gobby/workflows/engine.py +26 -4
  152. gobby/workflows/evaluator.py +8 -5
  153. gobby/workflows/lifecycle_evaluator.py +59 -27
  154. gobby/workflows/loader.py +567 -30
  155. gobby/workflows/lobster_compat.py +147 -0
  156. gobby/workflows/pipeline_executor.py +801 -0
  157. gobby/workflows/pipeline_state.py +172 -0
  158. gobby/workflows/pipeline_webhooks.py +206 -0
  159. gobby/workflows/premature_stop.py +5 -0
  160. gobby/worktrees/git.py +135 -20
  161. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/METADATA +56 -22
  162. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/RECORD +166 -122
  163. gobby/hooks/event_handlers.py +0 -1008
  164. gobby/mcp_proxy/tools/workflows.py +0 -1023
  165. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/WHEEL +0 -0
  166. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/entry_points.txt +0 -0
  167. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/licenses/LICENSE.md +0 -0
  168. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/top_level.txt +0 -0
@@ -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,
@@ -318,12 +311,16 @@ async def get_tool_schema(
318
311
  schema = registry.get_schema(tool_name)
319
312
  if schema:
320
313
  response_time_ms = (time.perf_counter() - start_time) * 1000
321
- return {
322
- "name": tool_name,
314
+ # Build response with description only if present
315
+ result: dict[str, Any] = {
316
+ "name": schema.get("name", tool_name),
317
+ "inputSchema": schema.get("inputSchema"),
323
318
  "server": server_name,
324
- "inputSchema": schema,
325
319
  "response_time_ms": response_time_ms,
326
320
  }
321
+ if schema.get("description"):
322
+ result["description"] = schema["description"]
323
+ return result
327
324
  raise HTTPException(
328
325
  status_code=404,
329
326
  detail={
@@ -340,15 +337,19 @@ async def get_tool_schema(
340
337
 
341
338
  # Get from external MCP server
342
339
  try:
343
- schema = await server.mcp_manager.get_tool_input_schema(server_name, tool_name)
340
+ tool_info = await server.mcp_manager.get_tool_info(server_name, tool_name)
344
341
  response_time_ms = (time.perf_counter() - start_time) * 1000
345
342
 
346
- return {
347
- "name": tool_name,
343
+ # Build response with description only if present
344
+ response: dict[str, Any] = {
345
+ "name": tool_info.get("name", tool_name),
346
+ "inputSchema": tool_info.get("inputSchema"),
348
347
  "server": server_name,
349
- "inputSchema": schema,
350
348
  "response_time_ms": response_time_ms,
351
349
  }
350
+ if tool_info.get("description"):
351
+ response["description"] = tool_info["description"]
352
+ return response
352
353
 
353
354
  except (KeyError, ValueError) as e:
354
355
  # Tool or server not found - 404
@@ -255,17 +255,17 @@ async def refresh_mcp_tools(
255
255
  try:
256
256
  session = await server.mcp_manager.ensure_connected(server_name)
257
257
  tools_result = await session.list_tools()
258
- for t in tools_result.tools:
258
+ for mcp_tool in tools_result.tools:
259
259
  schema = None
260
- if hasattr(t, "inputSchema"):
261
- if hasattr(t.inputSchema, "model_dump"):
262
- schema = t.inputSchema.model_dump()
263
- elif isinstance(t.inputSchema, dict):
264
- schema = t.inputSchema
260
+ if hasattr(mcp_tool, "inputSchema"):
261
+ if hasattr(mcp_tool.inputSchema, "model_dump"):
262
+ schema = mcp_tool.inputSchema.model_dump()
263
+ elif isinstance(mcp_tool.inputSchema, dict):
264
+ schema = mcp_tool.inputSchema
265
265
  tools.append(
266
266
  {
267
- "name": getattr(t, "name", ""),
268
- "description": getattr(t, "description", ""),
267
+ "name": getattr(mcp_tool, "name", ""),
268
+ "description": getattr(mcp_tool, "description", ""),
269
269
  "inputSchema": schema,
270
270
  }
271
271
  )
@@ -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