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.
- gobby/__init__.py +1 -1
- gobby/adapters/__init__.py +6 -0
- gobby/adapters/base.py +11 -2
- gobby/adapters/claude_code.py +5 -28
- gobby/adapters/codex_impl/adapter.py +38 -43
- gobby/adapters/copilot.py +324 -0
- gobby/adapters/cursor.py +373 -0
- gobby/adapters/gemini.py +2 -26
- gobby/adapters/windsurf.py +359 -0
- gobby/agents/definitions.py +162 -2
- gobby/agents/isolation.py +33 -1
- gobby/agents/pty_reader.py +192 -0
- gobby/agents/registry.py +10 -1
- gobby/agents/runner.py +24 -8
- gobby/agents/sandbox.py +8 -3
- gobby/agents/session.py +4 -0
- gobby/agents/spawn.py +9 -2
- gobby/agents/spawn_executor.py +49 -61
- gobby/agents/spawners/command_builder.py +4 -4
- gobby/app_context.py +64 -0
- gobby/cli/__init__.py +4 -0
- gobby/cli/install.py +259 -4
- gobby/cli/installers/__init__.py +12 -0
- gobby/cli/installers/copilot.py +242 -0
- gobby/cli/installers/cursor.py +244 -0
- gobby/cli/installers/shared.py +3 -0
- gobby/cli/installers/windsurf.py +242 -0
- gobby/cli/pipelines.py +639 -0
- gobby/cli/sessions.py +3 -1
- gobby/cli/skills.py +209 -0
- gobby/cli/tasks/crud.py +6 -5
- gobby/cli/tasks/search.py +1 -1
- gobby/cli/ui.py +116 -0
- gobby/cli/utils.py +5 -17
- gobby/cli/workflows.py +38 -17
- gobby/config/app.py +5 -0
- gobby/config/features.py +0 -20
- gobby/config/skills.py +23 -2
- gobby/config/tasks.py +4 -0
- gobby/hooks/broadcaster.py +9 -0
- gobby/hooks/event_handlers/__init__.py +155 -0
- gobby/hooks/event_handlers/_agent.py +175 -0
- gobby/hooks/event_handlers/_base.py +92 -0
- gobby/hooks/event_handlers/_misc.py +66 -0
- gobby/hooks/event_handlers/_session.py +487 -0
- gobby/hooks/event_handlers/_tool.py +196 -0
- gobby/hooks/events.py +48 -0
- gobby/hooks/hook_manager.py +27 -3
- gobby/install/copilot/hooks/hook_dispatcher.py +203 -0
- gobby/install/cursor/hooks/hook_dispatcher.py +203 -0
- gobby/install/gemini/hooks/hook_dispatcher.py +8 -0
- gobby/install/windsurf/hooks/hook_dispatcher.py +205 -0
- gobby/llm/__init__.py +14 -1
- gobby/llm/claude.py +594 -43
- gobby/llm/service.py +149 -0
- gobby/mcp_proxy/importer.py +4 -41
- gobby/mcp_proxy/instructions.py +9 -27
- gobby/mcp_proxy/manager.py +13 -3
- gobby/mcp_proxy/models.py +1 -0
- gobby/mcp_proxy/registries.py +66 -5
- gobby/mcp_proxy/server.py +6 -2
- gobby/mcp_proxy/services/recommendation.py +2 -28
- gobby/mcp_proxy/services/tool_filter.py +7 -0
- gobby/mcp_proxy/services/tool_proxy.py +19 -1
- gobby/mcp_proxy/stdio.py +37 -21
- gobby/mcp_proxy/tools/agents.py +7 -0
- gobby/mcp_proxy/tools/artifacts.py +3 -3
- gobby/mcp_proxy/tools/hub.py +30 -1
- gobby/mcp_proxy/tools/orchestration/cleanup.py +5 -5
- gobby/mcp_proxy/tools/orchestration/monitor.py +1 -1
- gobby/mcp_proxy/tools/orchestration/orchestrate.py +8 -3
- gobby/mcp_proxy/tools/orchestration/review.py +17 -4
- gobby/mcp_proxy/tools/orchestration/wait.py +7 -7
- gobby/mcp_proxy/tools/pipelines/__init__.py +254 -0
- gobby/mcp_proxy/tools/pipelines/_discovery.py +67 -0
- gobby/mcp_proxy/tools/pipelines/_execution.py +281 -0
- gobby/mcp_proxy/tools/sessions/_crud.py +4 -4
- gobby/mcp_proxy/tools/sessions/_handoff.py +1 -1
- gobby/mcp_proxy/tools/skills/__init__.py +184 -30
- gobby/mcp_proxy/tools/spawn_agent.py +229 -14
- gobby/mcp_proxy/tools/task_readiness.py +27 -4
- gobby/mcp_proxy/tools/tasks/_context.py +8 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +27 -1
- gobby/mcp_proxy/tools/tasks/_helpers.py +1 -1
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +125 -8
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +2 -1
- gobby/mcp_proxy/tools/tasks/_search.py +1 -1
- gobby/mcp_proxy/tools/workflows/__init__.py +273 -0
- gobby/mcp_proxy/tools/workflows/_artifacts.py +225 -0
- gobby/mcp_proxy/tools/workflows/_import.py +112 -0
- gobby/mcp_proxy/tools/workflows/_lifecycle.py +332 -0
- gobby/mcp_proxy/tools/workflows/_query.py +226 -0
- gobby/mcp_proxy/tools/workflows/_resolution.py +78 -0
- gobby/mcp_proxy/tools/workflows/_terminal.py +175 -0
- gobby/mcp_proxy/tools/worktrees.py +54 -15
- gobby/memory/components/__init__.py +0 -0
- gobby/memory/components/ingestion.py +98 -0
- gobby/memory/components/search.py +108 -0
- gobby/memory/context.py +5 -5
- gobby/memory/manager.py +16 -25
- gobby/paths.py +51 -0
- gobby/prompts/loader.py +1 -35
- gobby/runner.py +131 -16
- gobby/servers/http.py +193 -150
- gobby/servers/routes/__init__.py +2 -0
- gobby/servers/routes/admin.py +56 -0
- gobby/servers/routes/mcp/endpoints/execution.py +33 -32
- gobby/servers/routes/mcp/endpoints/registry.py +8 -8
- gobby/servers/routes/mcp/hooks.py +10 -1
- gobby/servers/routes/pipelines.py +227 -0
- gobby/servers/websocket.py +314 -1
- gobby/sessions/analyzer.py +89 -3
- gobby/sessions/manager.py +5 -5
- gobby/sessions/transcripts/__init__.py +3 -0
- gobby/sessions/transcripts/claude.py +5 -0
- gobby/sessions/transcripts/codex.py +5 -0
- gobby/sessions/transcripts/gemini.py +5 -0
- gobby/skills/hubs/__init__.py +25 -0
- gobby/skills/hubs/base.py +234 -0
- gobby/skills/hubs/claude_plugins.py +328 -0
- gobby/skills/hubs/clawdhub.py +289 -0
- gobby/skills/hubs/github_collection.py +465 -0
- gobby/skills/hubs/manager.py +263 -0
- gobby/skills/hubs/skillhub.py +342 -0
- gobby/skills/parser.py +23 -0
- gobby/skills/sync.py +5 -4
- gobby/storage/artifacts.py +19 -0
- gobby/storage/memories.py +4 -4
- gobby/storage/migrations.py +118 -3
- gobby/storage/pipelines.py +367 -0
- gobby/storage/sessions.py +23 -4
- gobby/storage/skills.py +48 -8
- gobby/storage/tasks/_aggregates.py +2 -2
- gobby/storage/tasks/_lifecycle.py +4 -4
- gobby/storage/tasks/_models.py +7 -1
- gobby/storage/tasks/_queries.py +3 -3
- gobby/sync/memories.py +4 -3
- gobby/tasks/commits.py +48 -17
- gobby/tasks/external_validator.py +4 -17
- gobby/tasks/validation.py +13 -87
- gobby/tools/summarizer.py +18 -51
- gobby/utils/status.py +13 -0
- gobby/workflows/actions.py +80 -0
- gobby/workflows/context_actions.py +265 -27
- gobby/workflows/definitions.py +119 -1
- gobby/workflows/detection_helpers.py +23 -11
- gobby/workflows/enforcement/__init__.py +11 -1
- gobby/workflows/enforcement/blocking.py +96 -0
- gobby/workflows/enforcement/handlers.py +35 -1
- gobby/workflows/enforcement/task_policy.py +18 -0
- gobby/workflows/engine.py +26 -4
- gobby/workflows/evaluator.py +8 -5
- gobby/workflows/lifecycle_evaluator.py +59 -27
- gobby/workflows/loader.py +567 -30
- gobby/workflows/lobster_compat.py +147 -0
- gobby/workflows/pipeline_executor.py +801 -0
- gobby/workflows/pipeline_state.py +172 -0
- gobby/workflows/pipeline_webhooks.py +206 -0
- gobby/workflows/premature_stop.py +5 -0
- gobby/worktrees/git.py +135 -20
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/METADATA +56 -22
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/RECORD +166 -122
- gobby/hooks/event_handlers.py +0 -1008
- gobby/mcp_proxy/tools/workflows.py +0 -1023
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/WHEEL +0 -0
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/licenses/LICENSE.md +0 -0
- {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
|
-
#
|
|
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
|
|
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
|
-
"
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
322
|
-
|
|
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
|
-
|
|
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
|
-
|
|
347
|
-
|
|
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
|
|
258
|
+
for mcp_tool in tools_result.tools:
|
|
259
259
|
schema = None
|
|
260
|
-
if hasattr(
|
|
261
|
-
if hasattr(
|
|
262
|
-
schema =
|
|
263
|
-
elif isinstance(
|
|
264
|
-
schema =
|
|
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(
|
|
268
|
-
"description": getattr(
|
|
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
|