rdev/cmd/rdev-api/openapi_ext.go
jordan 96219a647f
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
feat: add POST /projects/{id}/notify/reprovision to migrate notify host
Implements ReprovisionNotifyHost to migrate a project's email sending
from an old notify host to a new one (e.g., from project-name-based to
slug-based host). Preserves the project's notify account and send key.

- Adds ReprovisionNotifyHost to port.NotifyProvisioner interface
- Implements revokeHostAccess on notifyAdminAPI + adminClient
- Implements Provisioner.ReprovisionNotifyHost (12-step migration)
  in provisioner_reprovision.go (split to keep provisioner.go < 500 lines)
- Adds NotifyHandler.Reprovision handler (POST /notify/reprovision)
- Updates OpenAPI spec with reprovision endpoint

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 21:28:59 -07:00

1870 lines
59 KiB
Go

package main
import (
"fmt"
"strings"
"github.com/orchard9/rdev/pkg/api"
)
// wrapResponseExample wraps a bare JSON data value in the standard
// { "data": ..., "meta": {...} } response envelope used by all API endpoints.
func wrapResponseExample(dataExample string) string {
return fmt.Sprintf(`{
"data": %s,
"meta": {
"request_id": "req-550e8400-e29b",
"timestamp": "2026-01-27T12:00:00Z"
}
}`, dataExample)
}
// summaryToOperationID converts a human-readable summary to a camelCase operationId.
// "List API keys" → "listAPIKeys", "Health check" → "healthCheck"
func summaryToOperationID(summary string) string {
words := strings.Fields(summary)
var sb strings.Builder
first := true
for _, word := range words {
clean := strings.Map(func(r rune) rune {
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
return r
}
return -1
}, word)
if clean == "" {
continue
}
if first {
sb.WriteString(strings.ToLower(clean[:1]) + clean[1:])
first = false
} else {
sb.WriteString(strings.ToUpper(clean[:1]) + clean[1:])
}
}
return sb.String()
}
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.
// The example parameter is the bare data value (array or object); it is
// automatically wrapped in the standard { "data": ..., "meta": {...} } envelope.
func withAuth(summary, description, tag, scope, example string) map[string]any {
return map[string]any{
"operationId": summaryToOperationID(summary),
"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": wrapResponseExample(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.
// The responseExample parameter is the bare data value; it is automatically
// wrapped in the standard { "data": ..., "meta": {...} } envelope.
func withAuthAndBody(summary, description, tag, scope, requestExample, responseExample string) map[string]any {
return map[string]any{
"operationId": summaryToOperationID(summary),
"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": wrapResponseExample(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{
"operationId": summaryToOperationID(summary),
"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.
// The responseExample parameter is the bare data value; it is automatically
// wrapped in the standard { "data": ..., "meta": {...} } envelope.
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{
"operationId": summaryToOperationID(summary),
"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": wrapResponseExample(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{
"operationId": "getSdlcNextAction",
"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"
}`,
))
}
func registerComponentPaths(spec *api.OpenAPISpec) {
spec.AddPath("/projects/{id}/components", "get", withAuthAndParams(
"List components",
`Returns all components in a project's composable monorepo.
Each project can contain multiple components (services, workers, apps, CLIs) organized in a monorepo structure.`,
"Components",
"projects:read",
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
))
spec.AddPath("/projects/{id}/components", "post", withAuthBodyAndParams(
"Add component",
`Adds a new component to a project's monorepo.
Generates component skeleton from template, updates configuration, and commits changes.`,
"Components",
"projects:execute",
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
`{
"type": "service",
"name": "api-gateway",
"template": "go-rest-api",
"port": 8080
}`,
`{
"type": "service",
"name": "api-gateway",
"path": "services/api-gateway",
"port": 8080,
"template": "go-rest-api",
"dependencies": [],
"operation_id": "op-abc123"
}`,
))
spec.AddPath("/projects/{id}/components/batch", "post", withAuthBodyAndParams(
"Add multiple components",
`Adds multiple components to a project's monorepo atomically.
All components are created in a single transaction. If any component fails, the entire operation is rolled back.`,
"Components",
"projects:execute",
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
`{
"components": [
{"type": "service", "name": "auth", "port": 8081},
{"type": "service", "name": "users", "port": 8082},
{"type": "worker", "name": "email-processor"}
]
}`,
`{
"components": [
{
"type": "service",
"name": "auth",
"path": "services/auth",
"port": 8081,
"template": "go-rest-api",
"dependencies": []
},
{
"type": "service",
"name": "users",
"path": "services/users",
"port": 8082,
"template": "go-rest-api",
"dependencies": []
},
{
"type": "worker",
"name": "email-processor",
"path": "workers/email-processor",
"port": 0,
"template": "go-worker",
"dependencies": []
}
],
"operation_id": "op-abc123"
}`,
))
// Use wildcard for dynamic component path
spec.AddPath("/projects/{id}/components/{path}", "delete", map[string]any{
"operationId": "removeComponent",
"summary": "Remove component",
"description": "Removes a component from the project's monorepo.\n\n**Required scope**: `projects:execute`",
"tags": []string{"Components"},
"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": "path",
"in": "path",
"description": "Component path (e.g., services/api-gateway, workers/email-processor)",
"required": true,
"schema": map[string]any{"type": "string"},
},
},
"responses": map[string]any{
"204": map[string]any{"description": "No Content - Component removed"},
"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 - Component not found"},
},
})
}
func registerCredentialPaths(spec *api.OpenAPISpec) {
spec.AddPath("/credentials", "get", map[string]any{
"operationId": "listCredentials",
"summary": "List credentials",
"description": `Returns all infrastructure credentials with values masked.
Optionally filter by category using ?category=<name> query parameter.
**Required scope**: ` + "`admin`",
"tags": []string{"Credentials"},
"security": []map[string]any{
{"ApiKeyAuth": []string{}},
},
"parameters": []map[string]any{
{
"name": "category",
"in": "query",
"description": "Filter by category (e.g., git, ci, dns, registry)",
"required": false,
"schema": map[string]any{"type": "string"},
},
},
"responses": map[string]any{
"200": map[string]any{
"description": "Success",
"content": map[string]any{
"application/json": map[string]any{
"example": wrapResponseExample(`[
{
"key": "GITEA_TOKEN",
"value": "***",
"description": "Gitea API token for repo operations",
"category": "git",
"created_at": "2026-01-01T00:00:00Z",
"updated_at": "2026-01-15T10:30:00Z",
"updated_by": "admin"
}
]`),
},
},
},
"401": map[string]any{"description": "Unauthorized - Missing or invalid API key"},
"403": map[string]any{"description": "Forbidden - Admin scope required"},
},
})
spec.AddPath("/credentials/{key}", "get", withAuthAndParams(
"Get credential",
`Returns a single credential by key with full unmasked value.
**Security**: Only admin scope can retrieve unmasked credentials.`,
"Credentials",
"admin",
[]param{{Name: "key", In: "path", Description: "Credential key (e.g., GITEA_TOKEN, WOODPECKER_SECRET)", Required: true}},
))
spec.AddPath("/credentials", "post", withAuthAndBody(
"Set credential",
`Creates or updates a single credential.
Credentials are encrypted at rest in PostgreSQL.`,
"Credentials",
"admin",
`{
"key": "GITEA_TOKEN",
"value": "abc123xyz",
"description": "Gitea API token for repo operations",
"category": "git"
}`,
`{
"status": "stored",
"key": "GITEA_TOKEN"
}`,
))
spec.AddPath("/credentials/batch", "post", withAuthAndBody(
"Set multiple credentials",
`Creates or updates multiple credentials atomically.
Useful for bulk credential loading from configuration files.`,
"Credentials",
"admin",
`{
"credentials": [
{"key": "GITEA_TOKEN", "value": "abc123", "category": "git"},
{"key": "WOODPECKER_SECRET", "value": "xyz789", "category": "ci"}
]
}`,
`{
"status": "stored",
"count": 2,
"keys": ["GITEA_TOKEN", "WOODPECKER_SECRET"]
}`,
))
spec.AddPath("/credentials/{key}", "delete", map[string]any{
"operationId": "deleteCredential",
"summary": "Delete credential",
"description": "Removes a credential permanently.\n\n**Required scope**: `admin`",
"tags": []string{"Credentials"},
"security": []map[string]any{
{"ApiKeyAuth": []string{}},
},
"parameters": []map[string]any{
{
"name": "key",
"in": "path",
"description": "Credential key to delete",
"required": true,
"schema": map[string]any{"type": "string"},
},
},
"responses": map[string]any{
"200": map[string]any{
"description": "Success",
"content": map[string]any{
"application/json": map[string]any{
"example": wrapResponseExample(`{"status": "deleted", "key": "GITEA_TOKEN"}`),
},
},
},
"401": map[string]any{"description": "Unauthorized - Missing or invalid API key"},
"403": map[string]any{"description": "Forbidden - Admin scope required"},
"404": map[string]any{"description": "Not Found - Credential not found"},
},
})
}
func registerDiagnosticsPaths(spec *api.OpenAPISpec) {
spec.AddPath("/projects/{projectId}/diagnostics", "get", withAuthAndParams(
"Get project diagnostics",
`Returns comprehensive health and diagnostic information for a project.
Includes pod status, resource usage, recent errors, external system connectivity, and deployment health.`,
"Diagnostics",
"projects:read",
[]param{{Name: "projectId", In: "path", Description: "Project ID", Required: true}},
))
}
func registerVerifyPaths(spec *api.OpenAPISpec) {
spec.AddPath("/verify", "post", withAuthAndBody(
"Submit verification task",
`Submits a visual verification task using Playwright.
Captures screenshots at multiple viewports and optionally records video. Results include screenshot URLs and AI analysis of visual quality.`,
"Verify",
"verify:write",
`{
"project_id": "my-landing-page",
"url": "https://my-landing-page.threesix.ai",
"viewports": ["desktop", "tablet", "mobile"],
"wait_for": "networkidle",
"wait_timeout": 30000,
"full_page": true,
"video": true,
"callback_url": "https://myapp.com/webhooks/verify"
}`,
`{
"task_id": "verify-abc123",
"status_url": "/verify/verify-abc123",
"stream_url": "/verify/verify-abc123/stream"
}`,
))
spec.AddPath("/verify/{taskId}", "get", withAuthAndParams(
"Get verification status",
`Returns the status and results of a verification task.
Includes screenshot URLs, video URL (if recorded), and duration.`,
"Verify",
"verify:read",
[]param{{Name: "taskId", In: "path", Description: "Verification task ID", Required: true}},
))
spec.AddPath("/verify/{taskId}/stream", "get", map[string]any{
"operationId": "streamVerifyEvents",
"summary": "Stream verification events",
"description": `Streams real-time verification task events via Server-Sent Events (SSE).
Events include task started, screenshot captured, video recorded, task completed/failed.
**Required scope**: ` + "`verify:read`",
"tags": []string{"Verify"},
"security": []map[string]any{
{"ApiKeyAuth": []string{}},
},
"parameters": []map[string]any{
{
"name": "taskId",
"in": "path",
"description": "Verification task ID",
"required": true,
"schema": map[string]any{"type": "string"},
},
},
"responses": map[string]any{
"200": map[string]any{
"description": "Success - SSE stream",
"content": map[string]any{
"text/event-stream": map[string]any{
"example": `event: connected
data: {"task_id":"verify-abc123"}
event: started
data: {"viewport":"desktop","url":"https://my-landing-page.threesix.ai"}
event: screenshot_captured
data: {"viewport":"desktop","url":"https://storage/verify-abc123-desktop.png"}
event: completed
data: {"task_id":"verify-abc123","duration_ms":5432}`,
},
},
},
"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 - Task not found"},
},
})
spec.AddPath("/verify/{taskId}", "delete", withAuthAndParams(
"Cancel verification task",
`Cancels a pending or running verification task.
Stops Playwright browser and marks task as cancelled.`,
"Verify",
"verify:write",
[]param{{Name: "taskId", In: "path", Description: "Verification task ID", Required: true}},
))
spec.AddPath("/projects/{id}/verify", "get", map[string]any{
"operationId": "listProjectVerifications",
"summary": "List project verifications",
"description": `Returns verification tasks for a project with pagination.
**Required scope**: ` + "`verify:read`",
"tags": []string{"Verify"},
"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": "limit",
"in": "query",
"description": "Maximum number of results (default: 50)",
"required": false,
"schema": map[string]any{"type": "integer", "default": 50},
},
{
"name": "offset",
"in": "query",
"description": "Pagination offset (default: 0)",
"required": false,
"schema": map[string]any{"type": "integer", "default": 0},
},
},
"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"},
},
})
}
func registerInfrastructurePaths(spec *api.OpenAPISpec) {
// Git repository endpoints
spec.AddPath("/projects/{id}/repo", "post", withAuthBodyAndParams(
"Create repository",
`Creates a git repository for a project in Gitea.
Repository is created under the configured git owner (e.g., threesix organization).`,
"Infrastructure",
"projects:execute",
[]param{{Name: "id", In: "path", Description: "Project ID (will be repository name)", Required: true}},
`{
"description": "Landing page for product launch",
"private": false
}`,
`{
"id": 123,
"owner": "threesix",
"name": "my-landing-page",
"full_name": "threesix/my-landing-page",
"description": "Landing page for product launch",
"private": false,
"clone_ssh": "git@git.threesix.ai:threesix/my-landing-page.git",
"clone_http": "https://git.threesix.ai/threesix/my-landing-page.git",
"html_url": "https://git.threesix.ai/threesix/my-landing-page"
}`,
))
spec.AddPath("/projects/{id}/repo", "get", withAuthAndParams(
"Get repository",
`Returns repository information for a project.
Includes clone URLs, visibility, and metadata.`,
"Infrastructure",
"projects:read",
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
))
spec.AddPath("/projects/{id}/repo", "delete", withAuthAndParams(
"Delete repository",
`Deletes the git repository for a project.
**Warning**: This permanently deletes all git history and branches.`,
"Infrastructure",
"projects:execute",
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
))
// Deployment endpoints
spec.AddPath("/projects/{id}/deploy", "post", withAuthAndParams(
"Deploy project",
`Deploys a project to Kubernetes.
Creates deployment, service, and ingress resources.`,
"Infrastructure",
"projects:execute",
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
))
spec.AddPath("/projects/{id}/deploy/status", "get", withAuthAndParams(
"Get deployment status",
`Returns the current deployment status for a project.
Includes pod status, replica counts, and health information.`,
"Infrastructure",
"projects:read",
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
))
spec.AddPath("/projects/{id}/deploy", "delete", withAuthAndParams(
"Undeploy project",
`Removes deployment, service, and ingress resources for a project.
Pods are terminated and all Kubernetes resources are deleted.`,
"Infrastructure",
"projects:execute",
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
))
spec.AddPath("/projects/{id}/deploy/restart", "post", withAuthAndParams(
"Restart deployment",
`Restarts a project's deployment by recreating all pods.
Useful for applying configuration changes or recovering from errors.`,
"Infrastructure",
"projects:execute",
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
))
spec.AddPath("/projects/{id}/deploy/scale", "post", withAuthBodyAndParams(
"Scale deployment",
`Scales a project's deployment to the specified replica count.`,
"Infrastructure",
"projects:execute",
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
`{"replicas": 3}`,
`{"status": "scaled", "replicas": 3}`,
))
spec.AddPath("/projects/{id}/deploy/logs", "get", map[string]any{
"operationId": "getDeploymentLogs",
"summary": "Get deployment logs",
"description": `Returns recent logs from a project's deployment pods.
**Required scope**: ` + "`projects:read`",
"tags": []string{"Infrastructure"},
"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": "tail",
"in": "query",
"description": "Number of recent log lines to retrieve (default: 100)",
"required": false,
"schema": map[string]any{"type": "integer", "default": 100},
},
},
"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 - Project not found"},
},
})
// Domain endpoints
spec.AddPath("/projects/{id}/domain", "post", withAuthBodyAndParams(
"Add domain",
`Adds a custom domain to a project.
Creates DNS A record if domain is a subdomain of the configured base domain (e.g., threesix.ai).`,
"Infrastructure",
"projects:execute",
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
`{"domain": "custom.example.com"}`,
`{
"project": "my-landing-page",
"domain": "custom.example.com",
"status": "configured",
"note": "External domain configured. Point your DNS to 208.122.204.172"
}`,
))
spec.AddPath("/projects/{id}/domain", "delete", map[string]any{
"operationId": "removeProjectDomain",
"summary": "Remove domain",
"description": `Removes a custom domain from a project.
Deletes DNS A record if domain was a managed subdomain.
**Required scope**: ` + "`projects:execute`",
"tags": []string{"Infrastructure"},
"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": "domain",
"in": "query",
"description": "Domain to remove (e.g., custom.example.com)",
"required": true,
"schema": map[string]any{"type": "string"},
},
},
"responses": map[string]any{
"200": map[string]any{"description": "Success"},
"400": map[string]any{"description": "Bad Request - Missing domain parameter"},
"401": map[string]any{"description": "Unauthorized - Missing or invalid API key"},
"403": map[string]any{"description": "Forbidden - Insufficient permissions"},
},
})
spec.AddPath("/projects/{id}/domains", "get", withAuthAndParams(
"List domains",
`Returns all domains (primary and aliases) for a project.`,
"Infrastructure",
"projects:read",
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
))
spec.AddPath("/projects/{id}/domains", "post", withAuthBodyAndParams(
"Add domain alias",
`Adds an additional domain alias to a project.
Projects can have multiple domains pointing to the same deployment.`,
"Infrastructure",
"projects:execute",
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
`{"domain": "alias.example.com", "proxied": false}`,
`{"status": "added", "domain": "alias.example.com"}`,
))
spec.AddPath("/projects/{id}/domains/{domain}", "delete", withAuthAndParams(
"Remove domain alias",
`Removes a domain alias from a project.
Primary domain cannot be removed via this endpoint.`,
"Infrastructure",
"projects:execute",
[]param{
{Name: "id", In: "path", Description: "Project ID", Required: true},
{Name: "domain", In: "path", Description: "Domain to remove", Required: true},
},
))
// CI pipeline endpoints
spec.AddPath("/projects/{id}/pipelines", "get", withAuthAndParams(
"List CI pipelines",
`Returns recent CI pipeline runs for a project.
Includes build status, branch, commit info, and duration.`,
"Infrastructure",
"projects:read",
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
))
spec.AddPath("/projects/{id}/pipelines/{number}", "get", withAuthAndParams(
"Get pipeline details",
`Returns detailed information for a specific pipeline run.`,
"Infrastructure",
"projects:read",
[]param{
{Name: "id", In: "path", Description: "Project ID", Required: true},
{Name: "number", In: "path", Description: "Pipeline number", Required: true},
},
))
spec.AddPath("/projects/{id}/pipelines/{number}/steps", "get", withAuthAndParams(
"Get pipeline steps",
`Returns the steps (stages) of a pipeline run with status and logs.`,
"Infrastructure",
"projects:read",
[]param{
{Name: "id", In: "path", Description: "Project ID", Required: true},
{Name: "number", In: "path", Description: "Pipeline number", Required: true},
},
))
spec.AddPath("/projects/{id}/pipelines/{number}/retry", "post", withAuthAndParams(
"Retry pipeline",
`Retries a failed pipeline from the failed step.
Part of the enterprise resilience architecture for handling transient failures.`,
"Infrastructure",
"projects:execute",
[]param{
{Name: "id", In: "path", Description: "Project ID", Required: true},
{Name: "number", In: "path", Description: "Pipeline number to retry", Required: true},
},
))
}
func registerWebhookPaths(spec *api.OpenAPISpec) {
spec.AddPath("/webhooks/woodpecker", "post", map[string]any{
"operationId": "receiveWoodpeckerWebhook",
"summary": "Woodpecker CI webhook",
"description": `Receives build event webhooks from Woodpecker CI.
**Authentication**: Uses HMAC-SHA256 signature verification (X-Woodpecker-Signature header), not API key auth.
Processes successful builds on main/master branches to trigger deployments and DNS updates.`,
"tags": []string{"Webhooks"},
"parameters": []map[string]any{
{
"name": "X-Woodpecker-Signature",
"in": "header",
"description": "HMAC-SHA256 signature of request body",
"required": false,
"schema": map[string]any{"type": "string"},
},
},
"requestBody": map[string]any{
"required": true,
"content": map[string]any{
"application/json": map[string]any{
"example": `{
"event": "push",
"repo": {
"owner": "threesix",
"name": "my-landing-page",
"full_name": "threesix/my-landing-page"
},
"build": {
"number": 42,
"status": "success",
"branch": "main",
"commit": "abc123def456",
"message": "Add feature X",
"author": "jordan"
}
}`,
},
},
},
"responses": map[string]any{
"200": map[string]any{
"description": "Success - Event processed",
"content": map[string]any{
"application/json": map[string]any{
"example": wrapResponseExample(`{
"status": "success",
"project": "my-landing-page",
"image": "registry.threesix.ai/my-landing-page:abc123de",
"commit": "abc123def456",
"note": "component deployments managed by CI pipeline"
}`),
},
},
},
"400": map[string]any{"description": "Bad Request - Invalid payload"},
"401": map[string]any{"description": "Unauthorized - Invalid signature"},
},
})
}
func registerSagaPaths(spec *api.OpenAPISpec) {
spec.AddPath("/sagas", "post", withAuthAndBody(
"Create and start saga",
`Creates a new saga workflow and starts execution.
Sagas are distributed workflows with automatic compensation on failure. Useful for multi-step operations like project creation with rollback support.`,
"Sagas",
"projects:execute",
`{
"name": "create-full-stack-project",
"max_retries": 3,
"steps": [
{
"name": "create-repo",
"action": "api",
"compensate": "delete-repo",
"config": {"method": "POST", "endpoint": "/projects/{id}/repo"}
},
{
"name": "create-db",
"action": "api",
"depends_on": ["create-repo"],
"compensate": "delete-db",
"config": {"method": "POST", "endpoint": "/projects/{id}/database"}
}
]
}`,
`{
"id": "saga-abc123",
"name": "create-full-stack-project",
"status": "running",
"current_step": "create-repo",
"retry_count": 0,
"max_retries": 3,
"created_at": "2026-02-08T10:00:00Z",
"updated_at": "2026-02-08T10:00:00Z"
}`,
))
spec.AddPath("/sagas", "get", map[string]any{
"operationId": "listSagas",
"summary": "List sagas",
"description": `Returns sagas with optional filtering.
**Query parameters**: ?name=<name>&status=<status>
**Required scope**: ` + "`projects:read`",
"tags": []string{"Sagas"},
"security": []map[string]any{
{"ApiKeyAuth": []string{}},
},
"parameters": []map[string]any{
{
"name": "name",
"in": "query",
"description": "Filter by saga name",
"required": false,
"schema": map[string]any{"type": "string"},
},
{
"name": "status",
"in": "query",
"description": "Filter by status (pending, running, completed, failed, compensating, compensated)",
"required": false,
"schema": map[string]any{"type": "string"},
},
},
"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"},
},
})
spec.AddPath("/sagas/{id}", "get", withAuthAndParams(
"Get saga",
`Returns detailed saga execution state including all steps, outputs, and errors.`,
"Sagas",
"projects:read",
[]param{{Name: "id", In: "path", Description: "Saga ID", Required: true}},
))
spec.AddPath("/sagas/{id}", "delete", withAuthAndParams(
"Delete saga",
`Deletes a saga and all associated state.
Running sagas should be cancelled before deletion.`,
"Sagas",
"projects:execute",
[]param{{Name: "id", In: "path", Description: "Saga ID", Required: true}},
))
spec.AddPath("/sagas/{id}/retry", "post", withAuthAndParams(
"Retry failed saga",
`Resumes a failed saga from the last failed step.
Increments retry count and attempts to continue execution.`,
"Sagas",
"projects:execute",
[]param{{Name: "id", In: "path", Description: "Saga ID", Required: true}},
))
spec.AddPath("/sagas/{id}/rollback", "post", withAuthAndParams(
"Rollback saga",
`Triggers compensation for a failed saga.
Runs compensation actions in reverse order to undo completed steps.`,
"Sagas",
"projects:execute",
[]param{{Name: "id", In: "path", Description: "Saga ID", Required: true}},
))
spec.AddPath("/sagas/{id}/steps/{step}/retry", "post", withAuthAndParams(
"Retry specific step",
`Retries a specific failed step within a saga.
Does not increment saga-level retry count.`,
"Sagas",
"projects:execute",
[]param{
{Name: "id", In: "path", Description: "Saga ID", Required: true},
{Name: "step", In: "path", Description: "Step name", Required: true},
},
))
spec.AddPath("/sagas/{id}/steps/{step}/skip", "post", withAuthAndParams(
"Skip step",
`Skips a failed step and continues saga execution.
Marks step as skipped and allows dependent steps to proceed.`,
"Sagas",
"projects:execute",
[]param{
{Name: "id", In: "path", Description: "Saga ID", Required: true},
{Name: "step", In: "path", Description: "Step name to skip", Required: true},
},
))
}
func registerConversationPaths(spec *api.OpenAPISpec) {
spec.AddPath("/projects/{id}/conversations", "post", withAuthBodyAndParams(
"Create conversation",
`Creates a new conversation for conversational project design.
Part of the Foundary Studio system for interactive requirements gathering.`,
"Foundary",
"projects:execute",
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
`{"title": "Landing page design discussion"}`,
`{
"id": "conv-abc123",
"project_id": "my-project",
"title": "Landing page design discussion",
"created_at": "2026-02-09T00:00:00Z",
"updated_at": "2026-02-09T00:00:00Z"
}`,
))
spec.AddPath("/projects/{id}/conversations", "get", withAuthAndParams(
"List conversations",
`Returns all conversations for a project.`,
"Foundary",
"projects:read",
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
))
spec.AddPath("/projects/{id}/conversations/{conversationId}", "get", withAuthAndParams(
"Get conversation",
`Returns a single conversation with all messages.`,
"Foundary",
"projects:read",
[]param{
{Name: "id", In: "path", Description: "Project ID", Required: true},
{Name: "conversationId", In: "path", Description: "Conversation ID", Required: true},
},
))
spec.AddPath("/projects/{id}/conversations/{conversationId}/messages", "post", withAuthBodyAndParams(
"Add message",
`Adds a message to a conversation.`,
"Foundary",
"projects:execute",
[]param{
{Name: "id", In: "path", Description: "Project ID", Required: true},
{Name: "conversationId", In: "path", Description: "Conversation ID", Required: true},
},
`{"role": "user", "content": "I need a modern landing page with dark mode"}`,
`{
"id": "msg-abc123",
"conversation_id": "conv-abc123",
"role": "user",
"content": "I need a modern landing page with dark mode",
"created_at": "2026-02-09T00:00:00Z"
}`,
))
spec.AddPath("/projects/{id}/conversations/{conversationId}/messages", "get", withAuthAndParams(
"List messages",
`Returns all messages in a conversation.`,
"Foundary",
"projects:read",
[]param{
{Name: "id", In: "path", Description: "Project ID", Required: true},
{Name: "conversationId", In: "path", Description: "Conversation ID", Required: true},
},
))
spec.AddPath("/projects/{id}/conversations/{conversationId}", "delete", withAuthAndParams(
"Delete conversation",
`Deletes a conversation and all its messages.`,
"Foundary",
"projects:execute",
[]param{
{Name: "id", In: "path", Description: "Project ID", Required: true},
{Name: "conversationId", In: "path", Description: "Conversation ID", Required: true},
},
))
}
func registerBlueprintPaths(spec *api.OpenAPISpec) {
spec.AddPath("/projects/{id}/blueprints", "post", withAuthBodyAndParams(
"Create blueprint",
`Creates a structured project blueprint with JSONB spec storage.
Blueprints capture the technical specification extracted from conversations.`,
"Foundary",
"projects:execute",
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
`{
"name": "Landing Page v1",
"description": "Modern landing page with Astro and Tailwind",
"spec": {
"stack": ["astro", "tailwind", "typescript"],
"components": ["hero", "features", "cta"],
"features": {"dark_mode": true, "responsive": true}
}
}`,
`{
"id": "bp-abc123",
"project_id": "my-project",
"name": "Landing Page v1",
"description": "Modern landing page with Astro and Tailwind",
"spec": {
"stack": ["astro", "tailwind", "typescript"],
"components": ["hero", "features", "cta"],
"features": {"dark_mode": true, "responsive": true}
},
"created_at": "2026-02-09T00:00:00Z",
"updated_at": "2026-02-09T00:00:00Z"
}`,
))
spec.AddPath("/projects/{id}/blueprints", "get", withAuthAndParams(
"List blueprints",
`Returns all blueprints for a project.`,
"Foundary",
"projects:read",
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
))
spec.AddPath("/projects/{id}/blueprints/{blueprintId}", "get", withAuthAndParams(
"Get blueprint",
`Returns a single blueprint with full spec.`,
"Foundary",
"projects:read",
[]param{
{Name: "id", In: "path", Description: "Project ID", Required: true},
{Name: "blueprintId", In: "path", Description: "Blueprint ID", Required: true},
},
))
spec.AddPath("/projects/{id}/blueprints/{blueprintId}", "put", withAuthBodyAndParams(
"Update blueprint",
`Updates a blueprint's metadata or spec.`,
"Foundary",
"projects:execute",
[]param{
{Name: "id", In: "path", Description: "Project ID", Required: true},
{Name: "blueprintId", In: "path", Description: "Blueprint ID", Required: true},
},
`{
"name": "Landing Page v2",
"spec": {
"stack": ["astro", "tailwind", "typescript"],
"features": {"dark_mode": true, "animations": true}
}
}`,
`{"message": "blueprint updated"}`,
))
spec.AddPath("/projects/{id}/blueprints/{blueprintId}", "delete", withAuthAndParams(
"Delete blueprint",
`Deletes a blueprint permanently.`,
"Foundary",
"projects:execute",
[]param{
{Name: "id", In: "path", Description: "Project ID", Required: true},
{Name: "blueprintId", In: "path", Description: "Blueprint ID", Required: true},
},
))
}
func registerArchitectPaths(spec *api.OpenAPISpec) {
spec.AddPath("/projects/{id}/architect/start", "post", withAuthBodyAndParams(
"Start architect conversation",
`Starts a new conversational design session with the AI architect.
The architect asks clarifying questions and guides requirements gathering.`,
"Foundary",
"projects:execute",
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
`{
"title": "E-commerce platform",
"initial_message": "I want to build a marketplace for handmade goods"
}`,
`{
"conversation_id": "conv-abc123",
"title": "E-commerce platform",
"response": "Great! A marketplace for handmade goods. Let me ask some questions to understand your vision better. What scale are you targeting? Will this be for local artisans or a global marketplace?"
}`,
))
spec.AddPath("/projects/{id}/architect/continue/{conversationId}", "post", withAuthBodyAndParams(
"Continue architect conversation",
`Continues an existing conversation with the AI architect.`,
"Foundary",
"projects:execute",
[]param{
{Name: "id", In: "path", Description: "Project ID", Required: true},
{Name: "conversationId", In: "path", Description: "Conversation ID", Required: true},
},
`{"message": "Global marketplace, starting with 100 sellers"}`,
`{
"conversation_id": "conv-abc123",
"response": "Understood. For a global marketplace with 100 sellers initially, we'll need to consider international payments, multi-currency support, and shipping logistics. What's your timeline for launch?"
}`,
))
spec.AddPath("/projects/{id}/architect/generate-blueprint/{conversationId}", "post", withAuthAndParams(
"Generate blueprint from conversation",
`Extracts a structured blueprint from the conversation history.
Uses AI to parse requirements into a technical specification.`,
"Foundary",
"projects:execute",
[]param{
{Name: "id", In: "path", Description: "Project ID", Required: true},
{Name: "conversationId", In: "path", Description: "Conversation ID", Required: true},
},
))
}
func registerQuestionPaths(spec *api.OpenAPISpec) {
spec.AddPath("/projects/{id}/questions", "post", withAuthBodyAndParams(
"Create question",
`Creates a structured question for the architect to ask the user.
Supports text, choice, multichoice, and yes/no question types.`,
"Foundary",
"projects:execute",
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
`{
"conversation_id": "conv-abc123",
"type": "choice",
"text": "What authentication method would you prefer?",
"choices": ["Email/Password", "OAuth (Google, GitHub)", "Magic Link", "Phone/SMS"],
"metadata": {"category": "auth"}
}`,
`{
"id": "q-abc123",
"conversation_id": "conv-abc123",
"project_id": "my-project",
"type": "choice",
"text": "What authentication method would you prefer?",
"choices": ["Email/Password", "OAuth (Google, GitHub)", "Magic Link", "Phone/SMS"],
"metadata": {"category": "auth"},
"created_at": "2026-02-09T00:00:00Z"
}`,
))
spec.AddPath("/projects/{id}/questions", "get", withAuthAndParams(
"List unanswered questions",
`Returns all unanswered questions for a project.`,
"Foundary",
"projects:read",
[]param{{Name: "id", In: "path", Description: "Project ID", Required: true}},
))
spec.AddPath("/projects/{id}/questions/{questionId}", "get", withAuthAndParams(
"Get question",
`Returns a single question by ID.`,
"Foundary",
"projects:read",
[]param{
{Name: "id", In: "path", Description: "Project ID", Required: true},
{Name: "questionId", In: "path", Description: "Question ID", Required: true},
},
))
spec.AddPath("/projects/{id}/questions/conversation/{conversationId}", "get", withAuthAndParams(
"List questions by conversation",
`Returns all questions (answered and unanswered) for a conversation.`,
"Foundary",
"projects:read",
[]param{
{Name: "id", In: "path", Description: "Project ID", Required: true},
{Name: "conversationId", In: "path", Description: "Conversation ID", Required: true},
},
))
spec.AddPath("/projects/{id}/questions/{questionId}/answer", "post", withAuthBodyAndParams(
"Answer question",
`Records an answer to a question.
Answer format depends on question type:
- text/yesno: single string in "answer" field
- choice: single string in "answer" field (must match a choice)
- multichoice: array of strings in "answer_choices" field`,
"Foundary",
"projects:execute",
[]param{
{Name: "id", In: "path", Description: "Project ID", Required: true},
{Name: "questionId", In: "path", Description: "Question ID", Required: true},
},
`{"answer": "OAuth (Google, GitHub)"}`,
`{"message": "question answered"}`,
))
spec.AddPath("/projects/{id}/questions/{questionId}", "delete", withAuthAndParams(
"Delete question",
`Deletes a question permanently.`,
"Foundary",
"projects:execute",
[]param{
{Name: "id", In: "path", Description: "Project ID", Required: true},
{Name: "questionId", In: "path", Description: "Question ID", Required: true},
},
))
}