rdev/cmd/rdev-api/openapi_ext.go
jordan 56e3f83955 feat: add auth scopes, OpenAPI docs, SDLC guides, and code quality improvements
- Add auth.RequireScope() to all handler routes for proper authorization
- Add SDLC OpenAPI endpoint documentation (state, features, tasks, branches, merge, archive, orchestrator)
- Add SDLC documentation guides (getting-started, cli-reference, api-reference, command-catalog)
- Add artifact_test.go for SDLC artifact coverage
- Add CLAUDE.md rules: auth scopes requirement, error wrapping with %w
- Fix error wrapping to use %w instead of %v throughout codebase
- Improve CLI merge command with conflict detection and resolution
- Fix handler tests to include auth middleware for RequireScope
- Add cookbook tree runner scripts for automated testing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 13:55:50 -07:00

659 lines
20 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 registerSDLCPaths(spec *api.OpenAPISpec) {
// State and classifier
spec.AddPath("/projects/{id}/sdlc/state", "get", withAuthAndParams(
"Get SDLC state",
"Returns the global SDLC state for a project.",
"SDLC",
"projects:read",
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
))
spec.AddPath("/projects/{id}/sdlc/next", "get", map[string]any{
"summary": "Get next action",
"description": "Returns the classifier's recommended next action.\n\n**Required scope**: `projects:read`",
"tags": []string{"SDLC"},
"security": []map[string]any{{"ApiKeyAuth": []string{}}},
"parameters": []map[string]any{
{"name": "id", "in": "path", "description": "Project ID", "required": true, "schema": map[string]any{"type": "string"}},
{"name": "feature", "in": "query", "description": "Feature slug (optional)", "schema": map[string]any{"type": "string"}},
},
"responses": map[string]any{
"200": map[string]any{"description": "Success"},
"401": map[string]any{"description": "Unauthorized"},
},
})
// Features
spec.AddPath("/projects/{id}/sdlc/features", "get", withAuthAndParams(
"List features",
"Returns all features in the project.",
"SDLC",
"projects:read",
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
))
spec.AddPath("/projects/{id}/sdlc/features", "post", withAuthBodyAndParams(
"Create feature",
"Creates a new feature in the draft phase.",
"SDLC",
"projects:execute",
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
`{"slug": "auth-flow", "title": "User Authentication Flow"}`,
`{"slug": "auth-flow", "title": "User Authentication Flow", "phase": "draft"}`,
))
spec.AddPath("/projects/{id}/sdlc/features/{slug}", "get", withAuthAndParams(
"Get feature",
"Returns details for a specific feature.",
"SDLC",
"projects:read",
[]param{
{Name: "id", In: "path", Description: "Project ID", Required: true},
{Name: "slug", In: "path", Description: "Feature slug", Required: true},
},
))
spec.AddPath("/projects/{id}/sdlc/features/{slug}", "delete", withAuthAndParams(
"Delete feature",
"Deletes a feature from the project.",
"SDLC",
"projects:execute",
[]param{
{Name: "id", In: "path", Description: "Project ID", Required: true},
{Name: "slug", In: "path", Description: "Feature slug", Required: true},
},
))
spec.AddPath("/projects/{id}/sdlc/features/{slug}/transition", "post", withAuthBodyAndParams(
"Transition feature",
"Moves a feature to a new phase.",
"SDLC",
"projects:execute",
[]param{
{Name: "id", In: "path", Description: "Project ID", Required: true},
{Name: "slug", In: "path", Description: "Feature slug", Required: true},
},
`{"phase": "specified"}`,
`{"status": "transitioned", "phase": "specified"}`,
))
spec.AddPath("/projects/{id}/sdlc/features/{slug}/block", "post", withAuthBodyAndParams(
"Block feature",
"Adds a blocker reason to a feature.",
"SDLC",
"projects:execute",
[]param{
{Name: "id", In: "path", Description: "Project ID", Required: true},
{Name: "slug", In: "path", Description: "Feature slug", Required: true},
},
`{"reason": "Waiting for API design"}`,
`{"status": "blocked"}`,
))
spec.AddPath("/projects/{id}/sdlc/features/{slug}/unblock", "post", withAuthAndParams(
"Unblock feature",
"Removes all blockers from a feature.",
"SDLC",
"projects:execute",
[]param{
{Name: "id", In: "path", Description: "Project ID", Required: true},
{Name: "slug", In: "path", Description: "Feature slug", Required: true},
},
))
// Artifacts
spec.AddPath("/projects/{id}/sdlc/features/{slug}/artifacts", "get", withAuthAndParams(
"Get artifact status",
"Returns artifact statuses for a feature.",
"SDLC",
"projects:read",
[]param{
{Name: "id", In: "path", Description: "Project ID", Required: true},
{Name: "slug", In: "path", Description: "Feature slug", Required: true},
},
))
spec.AddPath("/projects/{id}/sdlc/features/{slug}/artifacts/{type}/approve", "post", withAuthAndParams(
"Approve artifact",
"Approves a feature artifact.",
"SDLC",
"projects:execute",
[]param{
{Name: "id", In: "path", Description: "Project ID", Required: true},
{Name: "slug", In: "path", Description: "Feature slug", Required: true},
{Name: "type", In: "path", Description: "Artifact type (spec, design, tasks, qa_plan, review, audit, qa_results)", Required: true},
},
))
spec.AddPath("/projects/{id}/sdlc/features/{slug}/artifacts/{type}/reject", "post", withAuthAndParams(
"Reject artifact",
"Rejects a feature artifact.",
"SDLC",
"projects:execute",
[]param{
{Name: "id", In: "path", Description: "Project ID", Required: true},
{Name: "slug", In: "path", Description: "Feature slug", Required: true},
{Name: "type", In: "path", Description: "Artifact type", Required: true},
},
))
// Tasks
spec.AddPath("/projects/{id}/sdlc/features/{slug}/tasks", "get", withAuthAndParams(
"List tasks",
"Returns all tasks for a feature.",
"SDLC",
"projects:read",
[]param{
{Name: "id", In: "path", Description: "Project ID", Required: true},
{Name: "slug", In: "path", Description: "Feature slug", Required: true},
},
))
spec.AddPath("/projects/{id}/sdlc/features/{slug}/tasks", "post", withAuthBodyAndParams(
"Add task",
"Adds a new task to a feature.",
"SDLC",
"projects:execute",
[]param{
{Name: "id", In: "path", Description: "Project ID", Required: true},
{Name: "slug", In: "path", Description: "Feature slug", Required: true},
},
`{"title": "Implement login handler"}`,
`{"id": "task-001", "title": "Implement login handler", "status": "pending"}`,
))
spec.AddPath("/projects/{id}/sdlc/features/{slug}/tasks/{taskId}/start", "post", withAuthAndParams(
"Start task",
"Marks a task as in-progress.",
"SDLC",
"projects:execute",
[]param{
{Name: "id", In: "path", Description: "Project ID", Required: true},
{Name: "slug", In: "path", Description: "Feature slug", Required: true},
{Name: "taskId", In: "path", Description: "Task ID", Required: true},
},
))
spec.AddPath("/projects/{id}/sdlc/features/{slug}/tasks/{taskId}/complete", "post", withAuthAndParams(
"Complete task",
"Marks a task as complete.",
"SDLC",
"projects:execute",
[]param{
{Name: "id", In: "path", Description: "Project ID", Required: true},
{Name: "slug", In: "path", Description: "Feature slug", Required: true},
{Name: "taskId", In: "path", Description: "Task ID", Required: true},
},
))
spec.AddPath("/projects/{id}/sdlc/features/{slug}/tasks/{taskId}/block", "post", withAuthAndParams(
"Block task",
"Marks a task as blocked.",
"SDLC",
"projects:execute",
[]param{
{Name: "id", In: "path", Description: "Project ID", Required: true},
{Name: "slug", In: "path", Description: "Feature slug", Required: true},
{Name: "taskId", In: "path", Description: "Task ID", Required: true},
},
))
// Branch management
spec.AddPath("/projects/{id}/sdlc/features/{slug}/branch", "post", withAuthAndParams(
"Create branch",
"Creates a feature branch and its manifest.",
"SDLC",
"projects:execute",
[]param{
{Name: "id", In: "path", Description: "Project ID", Required: true},
{Name: "slug", In: "path", Description: "Feature slug", Required: true},
},
))
spec.AddPath("/projects/{id}/sdlc/features/{slug}/branch", "get", withAuthAndParams(
"Get branch status",
"Returns branch status and merge checklist.",
"SDLC",
"projects:read",
[]param{
{Name: "id", In: "path", Description: "Project ID", Required: true},
{Name: "slug", In: "path", Description: "Feature slug", Required: true},
},
))
spec.AddPath("/projects/{id}/sdlc/features/{slug}/branch/sync", "post", withAuthAndParams(
"Sync branch",
"Syncs feature branch with base branch.",
"SDLC",
"projects:execute",
[]param{
{Name: "id", In: "path", Description: "Project ID", Required: true},
{Name: "slug", In: "path", Description: "Feature slug", Required: true},
},
))
// Merge and archive
spec.AddPath("/projects/{id}/sdlc/features/{slug}/merge", "post", withAuthBodyAndParams(
"Merge feature",
"Merges a feature branch after all gates pass.",
"SDLC",
"projects:execute",
[]param{
{Name: "id", In: "path", Description: "Project ID", Required: true},
{Name: "slug", In: "path", Description: "Feature slug", Required: true},
},
`{"strategy": "squash"}`,
`{"status": "merged", "strategy": "squash"}`,
))
spec.AddPath("/projects/{id}/sdlc/features/{slug}/archive", "post", withAuthAndParams(
"Archive feature",
"Archives a released feature.",
"SDLC",
"projects:execute",
[]param{
{Name: "id", In: "path", Description: "Project ID", Required: true},
{Name: "slug", In: "path", Description: "Feature slug", Required: true},
},
))
// Query endpoints
spec.AddPath("/projects/{id}/sdlc/query/blocked", "get", withAuthAndParams(
"Query blocked features",
"Returns all blocked features.",
"SDLC",
"projects:read",
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
))
spec.AddPath("/projects/{id}/sdlc/query/ready", "get", withAuthAndParams(
"Query ready features",
"Returns features ready for work.",
"SDLC",
"projects:read",
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
))
spec.AddPath("/projects/{id}/sdlc/query/needs-approval", "get", withAuthAndParams(
"Query features needing approval",
"Returns features awaiting approval.",
"SDLC",
"projects:read",
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
))
// Orchestrator endpoints
spec.AddPath("/projects/{id}/sdlc/execute", "post", withAuthBodyAndParams(
"Execute action",
"Executes the classifier's recommended next action.",
"SDLC",
"projects:execute",
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
`{"feature": "auth-flow", "provider": "claude"}`,
`{"action": "CREATE_SPEC", "success": true, "output": "..."}`,
))
spec.AddPath("/projects/{id}/sdlc/resolve", "post", withAuthBodyAndParams(
"Resolve blocker",
"Resolves a feature blocker and re-classifies.",
"SDLC",
"projects:execute",
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
`{"feature": "auth-flow"}`,
`{"action": "TRANSITION", "success": true, "output": "Feature unblocked"}`,
))
spec.AddPath("/projects/{id}/sdlc/commit", "post", withAuthBodyAndParams(
"Commit changes",
"Commits and optionally pushes changes in the project.",
"SDLC",
"projects:execute",
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
`{"feature": "auth-flow", "message": "feat: implement login", "push": true}`,
`{"commit_sha": "abc123", "files_changed": ["handler.go"], "pushed": 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"
}`,
))
}