- Add ListPipelines/GetPipeline to CIProvider port with Woodpecker adapter
- Add DNS alias endpoints: GET/POST/DELETE /projects/{id}/domains
- Implement worker executor daemon, build executor, and git operations
- Add build service, worker service, and build audit tracking
- Add worker registry with PostgreSQL adapter and migration
- Add multi-provider code agent interface (Claude Code + OpenCode)
- Add create-and-build combo endpoint
- Update landing-page cookbook to reflect all gaps closed
- Fix tech debt: unified validation, auth scopes, error wrapping, slog patterns
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
343 lines
9.8 KiB
Go
343 lines
9.8 KiB
Go
package main
|
|
|
|
import "github.com/orchard9/rdev/pkg/api"
|
|
|
|
func registerAgentPaths(spec *api.OpenAPISpec) {
|
|
spec.AddPath("/agents", "get", withAuth(
|
|
"List code agents",
|
|
`Returns all registered code agent providers and their status.
|
|
|
|
Shows which agents are available, their supported models, and the current default.`,
|
|
"Code Agents",
|
|
"projects:read",
|
|
`{
|
|
"agents": [
|
|
{
|
|
"provider": "claudecode",
|
|
"name": "Claude Code",
|
|
"available": true,
|
|
"default": true,
|
|
"supported_models": ["claude-sonnet-4-20250514"],
|
|
"default_model": "claude-sonnet-4-20250514"
|
|
},
|
|
{
|
|
"provider": "opencode",
|
|
"name": "OpenCode",
|
|
"available": false,
|
|
"default": false,
|
|
"supported_models": ["gpt-4o", "claude-sonnet-4-20250514"],
|
|
"default_model": "claude-sonnet-4-20250514"
|
|
}
|
|
],
|
|
"default_agent": "claudecode",
|
|
"total_agents": 2,
|
|
"available_count": 1
|
|
}`,
|
|
))
|
|
|
|
spec.AddPath("/agents/health", "get", withAuth(
|
|
"Get agent health status",
|
|
`Returns the health status of all registered code agents.
|
|
|
|
Checks connectivity to each agent backend and reports availability.`,
|
|
"Code Agents",
|
|
"projects:read",
|
|
`{
|
|
"agents": [
|
|
{
|
|
"provider": "claudecode",
|
|
"name": "Claude Code",
|
|
"healthy": true,
|
|
"message": "available",
|
|
"latency": "125ms",
|
|
"checked_at": "2026-01-27T12:00:00Z"
|
|
},
|
|
{
|
|
"provider": "opencode",
|
|
"name": "OpenCode",
|
|
"healthy": false,
|
|
"message": "unavailable",
|
|
"latency": "5.002s",
|
|
"checked_at": "2026-01-27T12:00:00Z"
|
|
}
|
|
],
|
|
"healthy_count": 1,
|
|
"total_count": 2,
|
|
"default_agent": "claudecode",
|
|
"default_healthy": true
|
|
}`,
|
|
))
|
|
|
|
spec.AddPath("/agents/{provider}", "get", withAuthAndParams(
|
|
"Get agent capabilities",
|
|
`Returns detailed capabilities for a specific code agent provider.
|
|
|
|
Includes supported features, models, and configuration options.`,
|
|
"Code Agents",
|
|
"projects:read",
|
|
[]param{{Name: "provider", In: "path", Description: "Agent provider ID (e.g., 'claudecode', 'opencode')", Required: true}},
|
|
))
|
|
|
|
spec.AddPath("/agents/default", "post", withAuthAndBody(
|
|
"Set default agent",
|
|
`Changes the default code agent used for command execution.
|
|
|
|
The specified provider must be registered and ideally available.`,
|
|
"Code Agents",
|
|
"admin",
|
|
`{"provider": "opencode"}`,
|
|
`{
|
|
"default_agent": "opencode",
|
|
"message": "default agent updated"
|
|
}`,
|
|
))
|
|
}
|
|
|
|
// param represents an OpenAPI parameter.
|
|
type param struct {
|
|
Name string
|
|
In string
|
|
Description string
|
|
Required bool
|
|
}
|
|
|
|
// withAuth creates an operation that requires authentication.
|
|
func withAuth(summary, description, tag, scope, example string) map[string]any {
|
|
return map[string]any{
|
|
"summary": summary,
|
|
"description": description + "\n\n**Required scope**: `" + scope + "`",
|
|
"tags": []string{tag},
|
|
"security": []map[string]any{
|
|
{"ApiKeyAuth": []string{}},
|
|
},
|
|
"responses": map[string]any{
|
|
"200": map[string]any{
|
|
"description": "Success",
|
|
"content": map[string]any{
|
|
"application/json": map[string]any{
|
|
"example": example,
|
|
},
|
|
},
|
|
},
|
|
"401": map[string]any{"description": "Unauthorized - Missing or invalid API key"},
|
|
"403": map[string]any{"description": "Forbidden - Insufficient permissions"},
|
|
},
|
|
}
|
|
}
|
|
|
|
// withAuthAndBody creates an operation with auth and request body.
|
|
func withAuthAndBody(summary, description, tag, scope, requestExample, responseExample string) map[string]any {
|
|
return map[string]any{
|
|
"summary": summary,
|
|
"description": description + "\n\n**Required scope**: `" + scope + "`",
|
|
"tags": []string{tag},
|
|
"security": []map[string]any{
|
|
{"ApiKeyAuth": []string{}},
|
|
},
|
|
"requestBody": map[string]any{
|
|
"required": true,
|
|
"content": map[string]any{
|
|
"application/json": map[string]any{
|
|
"example": requestExample,
|
|
},
|
|
},
|
|
},
|
|
"responses": map[string]any{
|
|
"201": map[string]any{
|
|
"description": "Created",
|
|
"content": map[string]any{
|
|
"application/json": map[string]any{
|
|
"example": responseExample,
|
|
},
|
|
},
|
|
},
|
|
"400": map[string]any{"description": "Bad Request - Invalid input"},
|
|
"401": map[string]any{"description": "Unauthorized - Missing or invalid API key"},
|
|
"403": map[string]any{"description": "Forbidden - Insufficient permissions"},
|
|
},
|
|
}
|
|
}
|
|
|
|
// withAuthAndParams creates an operation with auth and path parameters.
|
|
func withAuthAndParams(summary, description, tag, scope string, params []param) map[string]any {
|
|
parameters := make([]map[string]any, len(params))
|
|
for i, p := range params {
|
|
parameters[i] = map[string]any{
|
|
"name": p.Name,
|
|
"in": p.In,
|
|
"description": p.Description,
|
|
"required": p.Required,
|
|
"schema": map[string]any{"type": "string"},
|
|
}
|
|
}
|
|
return map[string]any{
|
|
"summary": summary,
|
|
"description": description + "\n\n**Required scope**: `" + scope + "`",
|
|
"tags": []string{tag},
|
|
"security": []map[string]any{
|
|
{"ApiKeyAuth": []string{}},
|
|
},
|
|
"parameters": parameters,
|
|
"responses": map[string]any{
|
|
"200": map[string]any{"description": "Success"},
|
|
"401": map[string]any{"description": "Unauthorized - Missing or invalid API key"},
|
|
"403": map[string]any{"description": "Forbidden - Insufficient permissions"},
|
|
"404": map[string]any{"description": "Not Found"},
|
|
},
|
|
}
|
|
}
|
|
|
|
// withAuthBodyAndParams creates an operation with auth, body, and params.
|
|
func withAuthBodyAndParams(summary, description, tag, scope string, params []param, requestExample, responseExample string) map[string]any {
|
|
parameters := make([]map[string]any, len(params))
|
|
for i, p := range params {
|
|
parameters[i] = map[string]any{
|
|
"name": p.Name,
|
|
"in": p.In,
|
|
"description": p.Description,
|
|
"required": p.Required,
|
|
"schema": map[string]any{"type": "string"},
|
|
}
|
|
}
|
|
return map[string]any{
|
|
"summary": summary,
|
|
"description": description + "\n\n**Required scope**: `" + scope + "`",
|
|
"tags": []string{tag},
|
|
"security": []map[string]any{
|
|
{"ApiKeyAuth": []string{}},
|
|
},
|
|
"parameters": parameters,
|
|
"requestBody": map[string]any{
|
|
"required": true,
|
|
"content": map[string]any{
|
|
"application/json": map[string]any{
|
|
"example": requestExample,
|
|
},
|
|
},
|
|
},
|
|
"responses": map[string]any{
|
|
"201": map[string]any{
|
|
"description": "Created",
|
|
"content": map[string]any{
|
|
"application/json": map[string]any{
|
|
"example": responseExample,
|
|
},
|
|
},
|
|
},
|
|
"400": map[string]any{"description": "Bad Request - Invalid input"},
|
|
"401": map[string]any{"description": "Unauthorized - Missing or invalid API key"},
|
|
"403": map[string]any{"description": "Forbidden - Insufficient permissions"},
|
|
},
|
|
}
|
|
}
|
|
|
|
func registerWorkerPaths(spec *api.OpenAPISpec) {
|
|
spec.AddPath("/workers", "get", withAuth(
|
|
"List workers",
|
|
"Returns all registered workers in the pool with status summary.",
|
|
"Workers",
|
|
"admin",
|
|
`{
|
|
"workers": [
|
|
{
|
|
"id": "rdev-worker-0",
|
|
"hostname": "rdev-worker-0.rdev.svc",
|
|
"status": "idle",
|
|
"capabilities": ["build", "test", "deploy"],
|
|
"registered_at": "2026-01-27T12:00:00Z",
|
|
"last_heartbeat": "2026-01-27T12:05:00Z",
|
|
"version": "1.0.0"
|
|
}
|
|
],
|
|
"total": 1,
|
|
"summary": {"idle": 1, "busy": 0, "draining": 0, "offline": 0}
|
|
}`,
|
|
))
|
|
|
|
spec.AddPath("/workers/{workerId}", "get", withAuthAndParams(
|
|
"Get worker",
|
|
"Returns details for a specific worker.",
|
|
"Workers",
|
|
"admin",
|
|
[]param{{Name: "workerId", In: "path", Description: "Worker ID", Required: true}},
|
|
))
|
|
|
|
spec.AddPath("/workers/{workerId}/drain", "post", withAuthAndParams(
|
|
"Drain worker",
|
|
"Sets a worker to draining status. It will finish its current task but stop accepting new work.",
|
|
"Workers",
|
|
"admin",
|
|
[]param{{Name: "workerId", In: "path", Description: "Worker ID", Required: true}},
|
|
))
|
|
}
|
|
|
|
func registerBuildPaths(spec *api.OpenAPISpec) {
|
|
spec.AddPath("/projects/{id}/builds", "post", withAuthBodyAndParams(
|
|
"Start build",
|
|
"Enqueues a build task for a project. The build will be picked up by an available worker.",
|
|
"Builds",
|
|
"projects:execute",
|
|
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
|
|
`{
|
|
"prompt": "Build a landing page with Next.js and Tailwind CSS",
|
|
"template": "nextjs-landing",
|
|
"auto_commit": true,
|
|
"auto_push": true,
|
|
"callback_url": "https://example.com/webhook"
|
|
}`,
|
|
`{
|
|
"task_id": "task-abc123",
|
|
"project_id": "my-project",
|
|
"status": "pending",
|
|
"status_url": "/builds/task-abc123"
|
|
}`,
|
|
))
|
|
|
|
spec.AddPath("/projects/{id}/builds", "get", withAuthAndParams(
|
|
"List builds",
|
|
"Returns build history for a project.",
|
|
"Builds",
|
|
"projects:read",
|
|
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
|
|
))
|
|
|
|
spec.AddPath("/builds/{taskId}", "get", withAuthAndParams(
|
|
"Get build status",
|
|
"Returns the status and result of a specific build.",
|
|
"Builds",
|
|
"projects:read",
|
|
[]param{{Name: "taskId", In: "path", Description: "Build task ID", Required: true}},
|
|
))
|
|
|
|
spec.AddPath("/project/create-and-build", "post", withAuthAndBody(
|
|
"Create project and build",
|
|
`Creates a new project and immediately enqueues a build task.
|
|
|
|
Combines project creation (git repo, DNS, CI activation) with build submission in a single call.`,
|
|
"Builds",
|
|
"admin",
|
|
`{
|
|
"name": "my-landing-page",
|
|
"description": "Landing page for product launch",
|
|
"template": "nextjs-landing",
|
|
"prompt": "Build a modern landing page with hero, features, and CTA sections",
|
|
"auto_commit": true,
|
|
"auto_push": true
|
|
}`,
|
|
`{
|
|
"project_id": "my-landing-page",
|
|
"name": "my-landing-page",
|
|
"domain": "my-landing-page.threesix.ai",
|
|
"url": "https://my-landing-page.threesix.ai",
|
|
"git": {
|
|
"owner": "jordan",
|
|
"name": "my-landing-page",
|
|
"clone_http": "https://git.threesix.ai/jordan/my-landing-page.git"
|
|
},
|
|
"task_id": "task-abc123",
|
|
"status": "pending",
|
|
"status_url": "/builds/task-abc123"
|
|
}`,
|
|
))
|
|
}
|