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.
- gobby/__init__.py +1 -1
- gobby/adapters/__init__.py +6 -0
- gobby/adapters/base.py +11 -2
- gobby/adapters/claude_code.py +2 -2
- 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 +5 -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/workflows.py +38 -17
- gobby/config/app.py +5 -0
- gobby/config/skills.py +23 -2
- gobby/hooks/broadcaster.py +9 -0
- gobby/hooks/event_handlers/_base.py +6 -1
- gobby/hooks/event_handlers/_session.py +44 -130
- gobby/hooks/events.py +48 -0
- gobby/hooks/hook_manager.py +25 -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 +217 -1
- gobby/llm/service.py +149 -0
- gobby/mcp_proxy/instructions.py +9 -27
- gobby/mcp_proxy/models.py +1 -0
- gobby/mcp_proxy/registries.py +56 -9
- gobby/mcp_proxy/server.py +6 -2
- 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/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/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 +9 -2
- gobby/mcp_proxy/tools/workflows/_lifecycle.py +12 -1
- gobby/mcp_proxy/tools/workflows/_query.py +45 -26
- gobby/mcp_proxy/tools/workflows/_terminal.py +39 -3
- gobby/mcp_proxy/tools/worktrees.py +54 -15
- gobby/memory/context.py +5 -5
- gobby/runner.py +108 -6
- gobby/servers/http.py +7 -1
- gobby/servers/routes/__init__.py +2 -0
- gobby/servers/routes/admin.py +44 -0
- gobby/servers/routes/mcp/endpoints/execution.py +18 -25
- 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 +87 -1
- 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/storage/memories.py +4 -4
- gobby/storage/migrations.py +95 -3
- gobby/storage/pipelines.py +367 -0
- gobby/storage/sessions.py +23 -4
- gobby/storage/skills.py +1 -1
- 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/workflows/actions.py +75 -0
- gobby/workflows/context_actions.py +246 -5
- gobby/workflows/definitions.py +119 -1
- gobby/workflows/detection_helpers.py +23 -11
- gobby/workflows/enforcement/task_policy.py +18 -0
- gobby/workflows/engine.py +20 -1
- gobby/workflows/evaluator.py +8 -5
- gobby/workflows/lifecycle_evaluator.py +57 -26
- 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.9.dist-info → gobby-0.2.11.dist-info}/METADATA +56 -22
- {gobby-0.2.9.dist-info → gobby-0.2.11.dist-info}/RECORD +134 -106
- {gobby-0.2.9.dist-info → gobby-0.2.11.dist-info}/WHEEL +0 -0
- {gobby-0.2.9.dist-info → gobby-0.2.11.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.9.dist-info → gobby-0.2.11.dist-info}/licenses/LICENSE.md +0 -0
- {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
|
"""
|
gobby/servers/routes/__init__.py
CHANGED
|
@@ -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",
|
gobby/servers/routes/admin.py
CHANGED
|
@@ -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
|
-
#
|
|
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,
|
|
@@ -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
|