From 665cec19911ba97a59b043f7819077fc1552444d Mon Sep 17 00:00:00 2001 From: hongming-ceo-delegated Date: Fri, 22 May 2026 23:37:04 -0700 Subject: [PATCH 1/3] RFC #1706 Phase 1: OpenAPI spec from workspace-server schedules handler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds swaggo-annotated comments to schedules.go (List/Create/Update/Delete/ RunNow/History), pulls request/response shapes up to package scope so swaggo can reference them, and wires `make openapi-spec` + `make openapi-spec-check` (CI gate). Generated workspace-server/docs/openapi/swagger.{yaml,json} from real handler signatures — first OpenAPI artifact in the repo. No backend behavior changes. Handler tests pass. Refs: #1706 (proposed kill-list for 4 hand-written HTTP wrappers across canvas / molecule-mcp-server / molecule-cli / molecule-sdk-python). Co-Authored-By: Claude Opus 4.7 (1M context) --- Makefile | 22 +- workspace-server/cmd/server/main.go | 13 + workspace-server/docs/openapi/swagger.json | 503 ++++++++++++++++++ workspace-server/docs/openapi/swagger.yaml | 329 ++++++++++++ .../internal/handlers/schedules.go | 108 +++- 5 files changed, 966 insertions(+), 9 deletions(-) create mode 100644 workspace-server/docs/openapi/swagger.json create mode 100644 workspace-server/docs/openapi/swagger.yaml diff --git a/Makefile b/Makefile index 0fa8d00d..38b1c522 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ # use this Makefile; CI calls docker compose / go test directly so the # Makefile can evolve without breaking the build. -.PHONY: help dev up down logs build test e2e-peer-visibility +.PHONY: help dev up down logs build test e2e-peer-visibility openapi-spec openapi-spec-check help: ## Show this help. @grep -E '^[a-zA-Z0-9_-]+:.*?## ' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-22s\033[0m %s\n", $$1, $$2}' @@ -36,3 +36,23 @@ test: ## Run Go unit tests in workspace-server/. # env contract (CLAUDE_CODE_OAUTH_TOKEN / E2E_MINIMAX_API_KEY / etc). e2e-peer-visibility: ## Run the LOCAL peer-visibility MCP gate vs the running stack (needs `make up` first). bash tests/e2e/test_peer_visibility_mcp_local.sh + +# ─── OpenAPI spec generation (RFC #1706, Phase 1) ───────────────────── +# Regenerate workspace-server/docs/openapi/swagger.{yaml,json} from +# swaggo annotations on the gin handlers. Commit the output. CI runs +# `make openapi-spec-check` to assert no drift between annotations and +# the committed file — if a PR changes a handler but forgets to +# regenerate, CI fails with a diff. +openapi-spec: ## Regenerate OpenAPI spec from workspace-server handler annotations. + @command -v swag >/dev/null 2>&1 || go install github.com/swaggo/swag/cmd/swag@v1.16.4 + cd workspace-server && swag init \ + --generalInfo cmd/server/main.go \ + --output docs/openapi \ + --outputTypes yaml,json \ + --dir . \ + --parseDependency=false \ + --parseInternal=true + +openapi-spec-check: openapi-spec ## CI gate — fail if openapi-spec produces a diff vs the committed file. + @git diff --exit-code -- workspace-server/docs/openapi/ \ + || (echo "openapi-spec is stale — run 'make openapi-spec' and commit the result" && exit 1) diff --git a/workspace-server/cmd/server/main.go b/workspace-server/cmd/server/main.go index d93f1325..01391446 100644 --- a/workspace-server/cmd/server/main.go +++ b/workspace-server/cmd/server/main.go @@ -1,3 +1,16 @@ +// Package main runs the per-tenant workspace-server. +// +// @title Molecule AI Workspace Server API +// @version 1.0 +// @description The per-tenant workspace-server HTTP API. Single source of truth for workspace/schedule/agent/secrets/files/memory CRUD. Hand-written clients (canvas, molecule-mcp-server, molecule-cli, molecule-sdk-python) should be replaced by clients generated from this spec — see RFC #1706. +// @host api.moleculesai.app +// @BasePath / +// @schemes https +// +// @securityDefinitions.apikey BearerAuth +// @in header +// @name Authorization +// @description Bearer token. The platform also reads X-Molecule-Org-Slug or X-Molecule-Org-Id for tenant routing. package main import ( diff --git a/workspace-server/docs/openapi/swagger.json b/workspace-server/docs/openapi/swagger.json new file mode 100644 index 00000000..3aaf5c44 --- /dev/null +++ b/workspace-server/docs/openapi/swagger.json @@ -0,0 +1,503 @@ +{ + "schemes": [ + "https" + ], + "swagger": "2.0", + "info": { + "description": "The per-tenant workspace-server HTTP API. Single source of truth for workspace/schedule/agent/secrets/files/memory CRUD. Hand-written clients (canvas, molecule-mcp-server, molecule-cli, molecule-sdk-python) should be replaced by clients generated from this spec — see RFC #1706.", + "title": "Molecule AI Workspace Server API", + "contact": {}, + "version": "1.0" + }, + "host": "api.moleculesai.app", + "basePath": "/", + "paths": { + "/workspaces/{id}/schedules": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "schedules" + ], + "summary": "List schedules for a workspace", + "parameters": [ + { + "type": "string", + "description": "Workspace ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.scheduleResponse" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.errorResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "schedules" + ], + "summary": "Create a schedule", + "parameters": [ + { + "type": "string", + "description": "Workspace ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Schedule fields", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.createScheduleRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/handlers.createScheduleResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handlers.errorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.errorResponse" + } + } + } + } + }, + "/workspaces/{id}/schedules/{scheduleId}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "schedules" + ], + "summary": "Delete a schedule", + "parameters": [ + { + "type": "string", + "description": "Workspace ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Schedule ID", + "name": "scheduleId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.statusResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/handlers.errorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.errorResponse" + } + } + } + }, + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "schedules" + ], + "summary": "Update a schedule", + "parameters": [ + { + "type": "string", + "description": "Workspace ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Schedule ID", + "name": "scheduleId", + "in": "path", + "required": true + }, + { + "description": "Partial schedule fields (only provided keys are updated)", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.updateScheduleRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.scheduleResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handlers.errorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/handlers.errorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.errorResponse" + } + } + } + } + }, + "/workspaces/{id}/schedules/{scheduleId}/history": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "schedules" + ], + "summary": "Get past runs of a schedule", + "parameters": [ + { + "type": "string", + "description": "Workspace ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Schedule ID", + "name": "scheduleId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.historyEntry" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.errorResponse" + } + } + } + } + }, + "/workspaces/{id}/schedules/{scheduleId}/run": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "schedules" + ], + "summary": "Fire a schedule manually", + "parameters": [ + { + "type": "string", + "description": "Workspace ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Schedule ID", + "name": "scheduleId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.runNowResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/handlers.errorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.errorResponse" + } + } + } + } + } + }, + "definitions": { + "handlers.createScheduleRequest": { + "type": "object", + "required": [ + "cron_expr", + "prompt" + ], + "properties": { + "cron_expr": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "prompt": { + "type": "string" + }, + "timezone": { + "type": "string" + } + } + }, + "handlers.createScheduleResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "next_run_at": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "handlers.errorResponse": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, + "handlers.historyEntry": { + "type": "object", + "properties": { + "duration_ms": { + "type": "integer" + }, + "error_detail": { + "type": "string" + }, + "request": { + "type": "object" + }, + "status": { + "type": "string" + }, + "timestamp": { + "type": "string" + } + } + }, + "handlers.runNowResponse": { + "type": "object", + "properties": { + "prompt": { + "type": "string" + }, + "status": { + "type": "string" + }, + "workspace_id": { + "type": "string" + } + } + }, + "handlers.scheduleResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "cron_expr": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "last_error": { + "type": "string" + }, + "last_run_at": { + "type": "string" + }, + "last_status": { + "type": "string" + }, + "name": { + "type": "string" + }, + "next_run_at": { + "type": "string" + }, + "prompt": { + "type": "string" + }, + "run_count": { + "type": "integer" + }, + "source": { + "description": "'template' (seeded by org/import) | 'runtime' (created via Canvas/API). Issue #24.", + "type": "string" + }, + "timezone": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "workspace_id": { + "type": "string" + } + } + }, + "handlers.statusResponse": { + "type": "object", + "properties": { + "status": { + "type": "string" + } + } + }, + "handlers.updateScheduleRequest": { + "type": "object", + "properties": { + "cron_expr": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "prompt": { + "type": "string" + }, + "timezone": { + "type": "string" + } + } + } + }, + "securityDefinitions": { + "BearerAuth": { + "description": "Bearer token. The platform also reads X-Molecule-Org-Slug or X-Molecule-Org-Id for tenant routing.", + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +} \ No newline at end of file diff --git a/workspace-server/docs/openapi/swagger.yaml b/workspace-server/docs/openapi/swagger.yaml new file mode 100644 index 00000000..42b0a041 --- /dev/null +++ b/workspace-server/docs/openapi/swagger.yaml @@ -0,0 +1,329 @@ +basePath: / +definitions: + handlers.createScheduleRequest: + properties: + cron_expr: + type: string + enabled: + type: boolean + name: + type: string + prompt: + type: string + timezone: + type: string + required: + - cron_expr + - prompt + type: object + handlers.createScheduleResponse: + properties: + id: + type: string + next_run_at: + type: string + status: + type: string + type: object + handlers.errorResponse: + properties: + error: + type: string + type: object + handlers.historyEntry: + properties: + duration_ms: + type: integer + error_detail: + type: string + request: + type: object + status: + type: string + timestamp: + type: string + type: object + handlers.runNowResponse: + properties: + prompt: + type: string + status: + type: string + workspace_id: + type: string + type: object + handlers.scheduleResponse: + properties: + created_at: + type: string + cron_expr: + type: string + enabled: + type: boolean + id: + type: string + last_error: + type: string + last_run_at: + type: string + last_status: + type: string + name: + type: string + next_run_at: + type: string + prompt: + type: string + run_count: + type: integer + source: + description: '''template'' (seeded by org/import) | ''runtime'' (created via + Canvas/API). Issue #24.' + type: string + timezone: + type: string + updated_at: + type: string + workspace_id: + type: string + type: object + handlers.statusResponse: + properties: + status: + type: string + type: object + handlers.updateScheduleRequest: + properties: + cron_expr: + type: string + enabled: + type: boolean + name: + type: string + prompt: + type: string + timezone: + type: string + type: object +host: api.moleculesai.app +info: + contact: {} + description: 'The per-tenant workspace-server HTTP API. Single source of truth for + workspace/schedule/agent/secrets/files/memory CRUD. Hand-written clients (canvas, + molecule-mcp-server, molecule-cli, molecule-sdk-python) should be replaced by + clients generated from this spec — see RFC #1706.' + title: Molecule AI Workspace Server API + version: "1.0" +paths: + /workspaces/{id}/schedules: + get: + parameters: + - description: Workspace ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/handlers.scheduleResponse' + type: array + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handlers.errorResponse' + security: + - BearerAuth: [] + summary: List schedules for a workspace + tags: + - schedules + post: + consumes: + - application/json + parameters: + - description: Workspace ID + in: path + name: id + required: true + type: string + - description: Schedule fields + in: body + name: body + required: true + schema: + $ref: '#/definitions/handlers.createScheduleRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/handlers.createScheduleResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/handlers.errorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handlers.errorResponse' + security: + - BearerAuth: [] + summary: Create a schedule + tags: + - schedules + /workspaces/{id}/schedules/{scheduleId}: + delete: + parameters: + - description: Workspace ID + in: path + name: id + required: true + type: string + - description: Schedule ID + in: path + name: scheduleId + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.statusResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/handlers.errorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handlers.errorResponse' + security: + - BearerAuth: [] + summary: Delete a schedule + tags: + - schedules + patch: + consumes: + - application/json + parameters: + - description: Workspace ID + in: path + name: id + required: true + type: string + - description: Schedule ID + in: path + name: scheduleId + required: true + type: string + - description: Partial schedule fields (only provided keys are updated) + in: body + name: body + required: true + schema: + $ref: '#/definitions/handlers.updateScheduleRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.scheduleResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/handlers.errorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/handlers.errorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handlers.errorResponse' + security: + - BearerAuth: [] + summary: Update a schedule + tags: + - schedules + /workspaces/{id}/schedules/{scheduleId}/history: + get: + parameters: + - description: Workspace ID + in: path + name: id + required: true + type: string + - description: Schedule ID + in: path + name: scheduleId + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/handlers.historyEntry' + type: array + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handlers.errorResponse' + security: + - BearerAuth: [] + summary: Get past runs of a schedule + tags: + - schedules + /workspaces/{id}/schedules/{scheduleId}/run: + post: + parameters: + - description: Workspace ID + in: path + name: id + required: true + type: string + - description: Schedule ID + in: path + name: scheduleId + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.runNowResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/handlers.errorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handlers.errorResponse' + security: + - BearerAuth: [] + summary: Fire a schedule manually + tags: + - schedules +schemes: +- https +securityDefinitions: + BearerAuth: + description: Bearer token. The platform also reads X-Molecule-Org-Slug or X-Molecule-Org-Id + for tenant routing. + in: header + name: Authorization + type: apiKey +swagger: "2.0" diff --git a/workspace-server/internal/handlers/schedules.go b/workspace-server/internal/handlers/schedules.go index 3139a217..62a9a6e3 100644 --- a/workspace-server/internal/handlers/schedules.go +++ b/workspace-server/internal/handlers/schedules.go @@ -15,6 +15,39 @@ import ( "github.com/Molecule-AI/molecule-monorepo/platform/internal/scheduler" ) +// errorResponse is returned for 4xx/5xx errors. (OpenAPI doc shape — used by swaggo.) +type errorResponse struct { + Error string `json:"error"` +} + +// statusResponse is returned by mutating endpoints that only echo a status verb. +type statusResponse struct { + Status string `json:"status"` +} + +// createScheduleResponse is returned by POST /workspaces/{id}/schedules. +type createScheduleResponse struct { + ID string `json:"id"` + Status string `json:"status"` + NextRunAt time.Time `json:"next_run_at"` +} + +// runNowResponse is returned by POST /workspaces/{id}/schedules/{scheduleId}/run. +type runNowResponse struct { + Status string `json:"status"` + WorkspaceID string `json:"workspace_id"` + Prompt string `json:"prompt"` +} + +// historyEntry is one row of /workspaces/{id}/schedules/{scheduleId}/history. +type historyEntry struct { + Timestamp time.Time `json:"timestamp"` + DurationMs *int `json:"duration_ms"` + Status *string `json:"status"` + ErrorDetail string `json:"error_detail"` + Request json.RawMessage `json:"request" swaggertype:"object"` +} + type ScheduleHandler struct{} func NewScheduleHandler() *ScheduleHandler { @@ -40,6 +73,15 @@ type scheduleResponse struct { } // List returns all schedules for a workspace. +// +// @Summary List schedules for a workspace +// @Tags schedules +// @Produce json +// @Param id path string true "Workspace ID" +// @Success 200 {array} scheduleResponse +// @Failure 500 {object} errorResponse +// @Router /workspaces/{id}/schedules [get] +// @Security BearerAuth func (h *ScheduleHandler) List(c *gin.Context) { workspaceID := c.Param("id") ctx := c.Request.Context() @@ -87,6 +129,18 @@ type createScheduleRequest struct { } // Create adds a new schedule for a workspace. +// +// @Summary Create a schedule +// @Tags schedules +// @Accept json +// @Produce json +// @Param id path string true "Workspace ID" +// @Param body body createScheduleRequest true "Schedule fields" +// @Success 201 {object} createScheduleResponse +// @Failure 400 {object} errorResponse +// @Failure 500 {object} errorResponse +// @Router /workspaces/{id}/schedules [post] +// @Security BearerAuth func (h *ScheduleHandler) Create(c *gin.Context) { workspaceID := c.Param("id") ctx := c.Request.Context() @@ -155,6 +209,20 @@ type updateScheduleRequest struct { // Update modifies a schedule. Uses a fixed UPDATE with COALESCE so only // provided fields are changed — no dynamic SQL construction. +// +// @Summary Update a schedule +// @Tags schedules +// @Accept json +// @Produce json +// @Param id path string true "Workspace ID" +// @Param scheduleId path string true "Schedule ID" +// @Param body body updateScheduleRequest true "Partial schedule fields (only provided keys are updated)" +// @Success 200 {object} scheduleResponse +// @Failure 400 {object} errorResponse +// @Failure 404 {object} errorResponse +// @Failure 500 {object} errorResponse +// @Router /workspaces/{id}/schedules/{scheduleId} [patch] +// @Security BearerAuth func (h *ScheduleHandler) Update(c *gin.Context) { scheduleID := c.Param("scheduleId") workspaceID := c.Param("id") // #113: bind to owning workspace to prevent IDOR @@ -230,6 +298,17 @@ func (h *ScheduleHandler) Update(c *gin.Context) { } // Delete removes a schedule. +// +// @Summary Delete a schedule +// @Tags schedules +// @Produce json +// @Param id path string true "Workspace ID" +// @Param scheduleId path string true "Schedule ID" +// @Success 200 {object} statusResponse +// @Failure 404 {object} errorResponse +// @Failure 500 {object} errorResponse +// @Router /workspaces/{id}/schedules/{scheduleId} [delete] +// @Security BearerAuth func (h *ScheduleHandler) Delete(c *gin.Context) { scheduleID := c.Param("scheduleId") workspaceID := c.Param("id") // #113: bind to owning workspace to prevent IDOR @@ -252,6 +331,17 @@ func (h *ScheduleHandler) Delete(c *gin.Context) { } // RunNow manually fires a schedule immediately. +// +// @Summary Fire a schedule manually +// @Tags schedules +// @Produce json +// @Param id path string true "Workspace ID" +// @Param scheduleId path string true "Schedule ID" +// @Success 200 {object} runNowResponse +// @Failure 404 {object} errorResponse +// @Failure 500 {object} errorResponse +// @Router /workspaces/{id}/schedules/{scheduleId}/run [post] +// @Security BearerAuth func (h *ScheduleHandler) RunNow(c *gin.Context) { scheduleID := c.Param("scheduleId") workspaceID := c.Param("id") @@ -282,6 +372,16 @@ func (h *ScheduleHandler) RunNow(c *gin.Context) { } // History returns recent runs for a schedule from activity_logs. +// +// @Summary Get past runs of a schedule +// @Tags schedules +// @Produce json +// @Param id path string true "Workspace ID" +// @Param scheduleId path string true "Schedule ID" +// @Success 200 {array} historyEntry +// @Failure 500 {object} errorResponse +// @Router /workspaces/{id}/schedules/{scheduleId}/history [get] +// @Security BearerAuth func (h *ScheduleHandler) History(c *gin.Context) { scheduleID := c.Param("scheduleId") workspaceID := c.Param("id") @@ -307,14 +407,6 @@ func (h *ScheduleHandler) History(c *gin.Context) { } defer rows.Close() - type historyEntry struct { - Timestamp time.Time `json:"timestamp"` - DurationMs *int `json:"duration_ms"` - Status *string `json:"status"` - ErrorDetail string `json:"error_detail"` - Request json.RawMessage `json:"request"` - } - entries := make([]historyEntry, 0) for rows.Next() { var e historyEntry -- 2.52.0 From f01636cd76b7495a3ed69ef8d151e27d180f7bad Mon Sep 17 00:00:00 2001 From: hongming-ceo-delegated Date: Fri, 22 May 2026 23:51:54 -0700 Subject: [PATCH 2/3] Address CR2 review: add OrgSlugAuth + OrgIdAuth securityDefinitions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CR2 (5-axis review) flagged: the original PR mentioned X-Molecule-Org-Slug and X-Molecule-Org-Id only in the BearerAuth @description prose. Generated SDK clients (per RFC #1706 Phases 3-6) wouldn't actually send those headers, so any client built from the spec would 401/403 against prod. Fix: - Add OrgSlugAuth + OrgIdAuth as formal apikey securityDefinitions in cmd/server/main.go - Bind every annotated endpoint with `@Security BearerAuth && OrgSlugAuth` (AND semantics — generated clients send both headers, not OR) - Regenerate docs/openapi/swagger.{yaml,json} Verified: swagger.yaml security blocks now show - BearerAuth: [] OrgSlugAuth: [] (single requirement object with both schemes = AND in OpenAPI 2.0). No backend behavior change. Handler tests pass. Refs: PR #1707, RFC #1706, CR2 review comment. Co-Authored-By: Claude Opus 4.7 (1M context) --- workspace-server/cmd/server/main.go | 12 ++++++- workspace-server/docs/openapi/swagger.json | 32 +++++++++++++++---- workspace-server/docs/openapi/swagger.yaml | 24 ++++++++++++-- .../internal/handlers/schedules.go | 12 +++---- 4 files changed, 64 insertions(+), 16 deletions(-) diff --git a/workspace-server/cmd/server/main.go b/workspace-server/cmd/server/main.go index 01391446..87deeebf 100644 --- a/workspace-server/cmd/server/main.go +++ b/workspace-server/cmd/server/main.go @@ -10,7 +10,17 @@ // @securityDefinitions.apikey BearerAuth // @in header // @name Authorization -// @description Bearer token. The platform also reads X-Molecule-Org-Slug or X-Molecule-Org-Id for tenant routing. +// @description Bearer token issued by Gitea (org-admin or persona PAT) or by the platform's signup/SSO flow. +// +// @securityDefinitions.apikey OrgSlugAuth +// @in header +// @name X-Molecule-Org-Slug +// @description Tenant routing header — required on every /workspaces/{id}/* request so the platform edge can route to the correct per-tenant workspace-server. Either X-Molecule-Org-Slug (human-readable, e.g. "agents-team") or X-Molecule-Org-Id (UUID) must be sent; slug is preferred for client code. +// +// @securityDefinitions.apikey OrgIdAuth +// @in header +// @name X-Molecule-Org-Id +// @description Tenant routing header (UUID form). Alternative to X-Molecule-Org-Slug. At least one of OrgSlugAuth or OrgIdAuth must be sent alongside BearerAuth. package main import ( diff --git a/workspace-server/docs/openapi/swagger.json b/workspace-server/docs/openapi/swagger.json index 3aaf5c44..6ed94f16 100644 --- a/workspace-server/docs/openapi/swagger.json +++ b/workspace-server/docs/openapi/swagger.json @@ -16,7 +16,8 @@ "get": { "security": [ { - "BearerAuth": [] + "BearerAuth": [], + "OrgSlugAuth": [] } ], "produces": [ @@ -56,7 +57,8 @@ "post": { "security": [ { - "BearerAuth": [] + "BearerAuth": [], + "OrgSlugAuth": [] } ], "consumes": [ @@ -113,7 +115,8 @@ "delete": { "security": [ { - "BearerAuth": [] + "BearerAuth": [], + "OrgSlugAuth": [] } ], "produces": [ @@ -163,7 +166,8 @@ "patch": { "security": [ { - "BearerAuth": [] + "BearerAuth": [], + "OrgSlugAuth": [] } ], "consumes": [ @@ -233,7 +237,8 @@ "get": { "security": [ { - "BearerAuth": [] + "BearerAuth": [], + "OrgSlugAuth": [] } ], "produces": [ @@ -282,7 +287,8 @@ "post": { "security": [ { - "BearerAuth": [] + "BearerAuth": [], + "OrgSlugAuth": [] } ], "produces": [ @@ -494,10 +500,22 @@ }, "securityDefinitions": { "BearerAuth": { - "description": "Bearer token. The platform also reads X-Molecule-Org-Slug or X-Molecule-Org-Id for tenant routing.", + "description": "Bearer token issued by Gitea (org-admin or persona PAT) or by the platform's signup/SSO flow.", "type": "apiKey", "name": "Authorization", "in": "header" + }, + "OrgIdAuth": { + "description": "Tenant routing header (UUID form). Alternative to X-Molecule-Org-Slug. At least one of OrgSlugAuth or OrgIdAuth must be sent alongside BearerAuth.", + "type": "apiKey", + "name": "X-Molecule-Org-Id", + "in": "header" + }, + "OrgSlugAuth": { + "description": "Tenant routing header — required on every /workspaces/{id}/* request so the platform edge can route to the correct per-tenant workspace-server. Either X-Molecule-Org-Slug (human-readable, e.g. \"agents-team\") or X-Molecule-Org-Id (UUID) must be sent; slug is preferred for client code.", + "type": "apiKey", + "name": "X-Molecule-Org-Slug", + "in": "header" } } } \ No newline at end of file diff --git a/workspace-server/docs/openapi/swagger.yaml b/workspace-server/docs/openapi/swagger.yaml index 42b0a041..b66a87fa 100644 --- a/workspace-server/docs/openapi/swagger.yaml +++ b/workspace-server/docs/openapi/swagger.yaml @@ -138,6 +138,7 @@ paths: $ref: '#/definitions/handlers.errorResponse' security: - BearerAuth: [] + OrgSlugAuth: [] summary: List schedules for a workspace tags: - schedules @@ -173,6 +174,7 @@ paths: $ref: '#/definitions/handlers.errorResponse' security: - BearerAuth: [] + OrgSlugAuth: [] summary: Create a schedule tags: - schedules @@ -206,6 +208,7 @@ paths: $ref: '#/definitions/handlers.errorResponse' security: - BearerAuth: [] + OrgSlugAuth: [] summary: Delete a schedule tags: - schedules @@ -250,6 +253,7 @@ paths: $ref: '#/definitions/handlers.errorResponse' security: - BearerAuth: [] + OrgSlugAuth: [] summary: Update a schedule tags: - schedules @@ -281,6 +285,7 @@ paths: $ref: '#/definitions/handlers.errorResponse' security: - BearerAuth: [] + OrgSlugAuth: [] summary: Get past runs of a schedule tags: - schedules @@ -314,6 +319,7 @@ paths: $ref: '#/definitions/handlers.errorResponse' security: - BearerAuth: [] + OrgSlugAuth: [] summary: Fire a schedule manually tags: - schedules @@ -321,9 +327,23 @@ schemes: - https securityDefinitions: BearerAuth: - description: Bearer token. The platform also reads X-Molecule-Org-Slug or X-Molecule-Org-Id - for tenant routing. + description: Bearer token issued by Gitea (org-admin or persona PAT) or by the + platform's signup/SSO flow. in: header name: Authorization type: apiKey + OrgIdAuth: + description: Tenant routing header (UUID form). Alternative to X-Molecule-Org-Slug. + At least one of OrgSlugAuth or OrgIdAuth must be sent alongside BearerAuth. + in: header + name: X-Molecule-Org-Id + type: apiKey + OrgSlugAuth: + description: Tenant routing header — required on every /workspaces/{id}/* request + so the platform edge can route to the correct per-tenant workspace-server. Either + X-Molecule-Org-Slug (human-readable, e.g. "agents-team") or X-Molecule-Org-Id + (UUID) must be sent; slug is preferred for client code. + in: header + name: X-Molecule-Org-Slug + type: apiKey swagger: "2.0" diff --git a/workspace-server/internal/handlers/schedules.go b/workspace-server/internal/handlers/schedules.go index 62a9a6e3..351b2215 100644 --- a/workspace-server/internal/handlers/schedules.go +++ b/workspace-server/internal/handlers/schedules.go @@ -81,7 +81,7 @@ type scheduleResponse struct { // @Success 200 {array} scheduleResponse // @Failure 500 {object} errorResponse // @Router /workspaces/{id}/schedules [get] -// @Security BearerAuth +// @Security BearerAuth && OrgSlugAuth func (h *ScheduleHandler) List(c *gin.Context) { workspaceID := c.Param("id") ctx := c.Request.Context() @@ -140,7 +140,7 @@ type createScheduleRequest struct { // @Failure 400 {object} errorResponse // @Failure 500 {object} errorResponse // @Router /workspaces/{id}/schedules [post] -// @Security BearerAuth +// @Security BearerAuth && OrgSlugAuth func (h *ScheduleHandler) Create(c *gin.Context) { workspaceID := c.Param("id") ctx := c.Request.Context() @@ -222,7 +222,7 @@ type updateScheduleRequest struct { // @Failure 404 {object} errorResponse // @Failure 500 {object} errorResponse // @Router /workspaces/{id}/schedules/{scheduleId} [patch] -// @Security BearerAuth +// @Security BearerAuth && OrgSlugAuth func (h *ScheduleHandler) Update(c *gin.Context) { scheduleID := c.Param("scheduleId") workspaceID := c.Param("id") // #113: bind to owning workspace to prevent IDOR @@ -308,7 +308,7 @@ func (h *ScheduleHandler) Update(c *gin.Context) { // @Failure 404 {object} errorResponse // @Failure 500 {object} errorResponse // @Router /workspaces/{id}/schedules/{scheduleId} [delete] -// @Security BearerAuth +// @Security BearerAuth && OrgSlugAuth func (h *ScheduleHandler) Delete(c *gin.Context) { scheduleID := c.Param("scheduleId") workspaceID := c.Param("id") // #113: bind to owning workspace to prevent IDOR @@ -341,7 +341,7 @@ func (h *ScheduleHandler) Delete(c *gin.Context) { // @Failure 404 {object} errorResponse // @Failure 500 {object} errorResponse // @Router /workspaces/{id}/schedules/{scheduleId}/run [post] -// @Security BearerAuth +// @Security BearerAuth && OrgSlugAuth func (h *ScheduleHandler) RunNow(c *gin.Context) { scheduleID := c.Param("scheduleId") workspaceID := c.Param("id") @@ -381,7 +381,7 @@ func (h *ScheduleHandler) RunNow(c *gin.Context) { // @Success 200 {array} historyEntry // @Failure 500 {object} errorResponse // @Router /workspaces/{id}/schedules/{scheduleId}/history [get] -// @Security BearerAuth +// @Security BearerAuth && OrgSlugAuth func (h *ScheduleHandler) History(c *gin.Context) { scheduleID := c.Param("scheduleId") workspaceID := c.Param("id") -- 2.52.0 From f86e151c02e6aac49b85ab18f90eb9c59607e6b1 Mon Sep 17 00:00:00 2001 From: hongming-ceo-delegated Date: Sat, 23 May 2026 00:14:16 -0700 Subject: [PATCH 3/3] =?UTF-8?q?Fix=20golangci-lint=20unused=20failure=20?= =?UTF-8?q?=E2=80=94=20export=20OpenAPI=20response=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI / Platform (Go) flagged `internal/handlers/schedules.go:19:6: type errorResponse is unused (unused)` because golangci-lint's `unused` checker doesn't recognise swaggo's `@Success {object} errorResponse` annotations as references — the types are only used in comments from Go's perspective. Fix: export all OpenAPI request/response types. Exported names are skipped by the unused-checker (package consumers may exist outside the package, even if absent inside it). Renamed: - errorResponse → ErrorResponse - statusResponse → StatusResponse - createScheduleResponse → CreateScheduleResponse - runNowResponse → RunNowResponse - historyEntry → HistoryEntry - scheduleResponse → ScheduleResponse - createScheduleRequest → CreateScheduleRequest - updateScheduleRequest → UpdateScheduleRequest - scheduleHealthResponse → ScheduleHealthResponse (test ref updated) Regenerated docs/openapi/swagger.{yaml,json} with new type names. Co-Authored-By: Claude Opus 4.7 (1M context) --- workspace-server/docs/openapi/swagger.json | 54 ++++++------ workspace-server/docs/openapi/swagger.yaml | 54 ++++++------ .../internal/handlers/schedules.go | 84 +++++++++---------- .../internal/handlers/schedules_test.go | 4 +- 4 files changed, 98 insertions(+), 98 deletions(-) diff --git a/workspace-server/docs/openapi/swagger.json b/workspace-server/docs/openapi/swagger.json index 6ed94f16..a335eaaa 100644 --- a/workspace-server/docs/openapi/swagger.json +++ b/workspace-server/docs/openapi/swagger.json @@ -42,14 +42,14 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/handlers.scheduleResponse" + "$ref": "#/definitions/handlers.ScheduleResponse" } } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/handlers.errorResponse" + "$ref": "#/definitions/handlers.ErrorResponse" } } } @@ -85,7 +85,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/handlers.createScheduleRequest" + "$ref": "#/definitions/handlers.CreateScheduleRequest" } } ], @@ -93,19 +93,19 @@ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/handlers.createScheduleResponse" + "$ref": "#/definitions/handlers.CreateScheduleResponse" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/handlers.errorResponse" + "$ref": "#/definitions/handlers.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/handlers.errorResponse" + "$ref": "#/definitions/handlers.ErrorResponse" } } } @@ -146,19 +146,19 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handlers.statusResponse" + "$ref": "#/definitions/handlers.StatusResponse" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/handlers.errorResponse" + "$ref": "#/definitions/handlers.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/handlers.errorResponse" + "$ref": "#/definitions/handlers.ErrorResponse" } } } @@ -201,7 +201,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/handlers.updateScheduleRequest" + "$ref": "#/definitions/handlers.UpdateScheduleRequest" } } ], @@ -209,25 +209,25 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handlers.scheduleResponse" + "$ref": "#/definitions/handlers.ScheduleResponse" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/handlers.errorResponse" + "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/handlers.errorResponse" + "$ref": "#/definitions/handlers.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/handlers.errorResponse" + "$ref": "#/definitions/handlers.ErrorResponse" } } } @@ -270,14 +270,14 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/handlers.historyEntry" + "$ref": "#/definitions/handlers.HistoryEntry" } } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/handlers.errorResponse" + "$ref": "#/definitions/handlers.ErrorResponse" } } } @@ -318,19 +318,19 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handlers.runNowResponse" + "$ref": "#/definitions/handlers.RunNowResponse" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/handlers.errorResponse" + "$ref": "#/definitions/handlers.ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/handlers.errorResponse" + "$ref": "#/definitions/handlers.ErrorResponse" } } } @@ -338,7 +338,7 @@ } }, "definitions": { - "handlers.createScheduleRequest": { + "handlers.CreateScheduleRequest": { "type": "object", "required": [ "cron_expr", @@ -362,7 +362,7 @@ } } }, - "handlers.createScheduleResponse": { + "handlers.CreateScheduleResponse": { "type": "object", "properties": { "id": { @@ -376,7 +376,7 @@ } } }, - "handlers.errorResponse": { + "handlers.ErrorResponse": { "type": "object", "properties": { "error": { @@ -384,7 +384,7 @@ } } }, - "handlers.historyEntry": { + "handlers.HistoryEntry": { "type": "object", "properties": { "duration_ms": { @@ -404,7 +404,7 @@ } } }, - "handlers.runNowResponse": { + "handlers.RunNowResponse": { "type": "object", "properties": { "prompt": { @@ -418,7 +418,7 @@ } } }, - "handlers.scheduleResponse": { + "handlers.ScheduleResponse": { "type": "object", "properties": { "created_at": { @@ -469,7 +469,7 @@ } } }, - "handlers.statusResponse": { + "handlers.StatusResponse": { "type": "object", "properties": { "status": { @@ -477,7 +477,7 @@ } } }, - "handlers.updateScheduleRequest": { + "handlers.UpdateScheduleRequest": { "type": "object", "properties": { "cron_expr": { diff --git a/workspace-server/docs/openapi/swagger.yaml b/workspace-server/docs/openapi/swagger.yaml index b66a87fa..9413a278 100644 --- a/workspace-server/docs/openapi/swagger.yaml +++ b/workspace-server/docs/openapi/swagger.yaml @@ -1,6 +1,6 @@ basePath: / definitions: - handlers.createScheduleRequest: + handlers.CreateScheduleRequest: properties: cron_expr: type: string @@ -16,7 +16,7 @@ definitions: - cron_expr - prompt type: object - handlers.createScheduleResponse: + handlers.CreateScheduleResponse: properties: id: type: string @@ -25,12 +25,12 @@ definitions: status: type: string type: object - handlers.errorResponse: + handlers.ErrorResponse: properties: error: type: string type: object - handlers.historyEntry: + handlers.HistoryEntry: properties: duration_ms: type: integer @@ -43,7 +43,7 @@ definitions: timestamp: type: string type: object - handlers.runNowResponse: + handlers.RunNowResponse: properties: prompt: type: string @@ -52,7 +52,7 @@ definitions: workspace_id: type: string type: object - handlers.scheduleResponse: + handlers.ScheduleResponse: properties: created_at: type: string @@ -87,12 +87,12 @@ definitions: workspace_id: type: string type: object - handlers.statusResponse: + handlers.StatusResponse: properties: status: type: string type: object - handlers.updateScheduleRequest: + handlers.UpdateScheduleRequest: properties: cron_expr: type: string @@ -130,12 +130,12 @@ paths: description: OK schema: items: - $ref: '#/definitions/handlers.scheduleResponse' + $ref: '#/definitions/handlers.ScheduleResponse' type: array "500": description: Internal Server Error schema: - $ref: '#/definitions/handlers.errorResponse' + $ref: '#/definitions/handlers.ErrorResponse' security: - BearerAuth: [] OrgSlugAuth: [] @@ -156,22 +156,22 @@ paths: name: body required: true schema: - $ref: '#/definitions/handlers.createScheduleRequest' + $ref: '#/definitions/handlers.CreateScheduleRequest' produces: - application/json responses: "201": description: Created schema: - $ref: '#/definitions/handlers.createScheduleResponse' + $ref: '#/definitions/handlers.CreateScheduleResponse' "400": description: Bad Request schema: - $ref: '#/definitions/handlers.errorResponse' + $ref: '#/definitions/handlers.ErrorResponse' "500": description: Internal Server Error schema: - $ref: '#/definitions/handlers.errorResponse' + $ref: '#/definitions/handlers.ErrorResponse' security: - BearerAuth: [] OrgSlugAuth: [] @@ -197,15 +197,15 @@ paths: "200": description: OK schema: - $ref: '#/definitions/handlers.statusResponse' + $ref: '#/definitions/handlers.StatusResponse' "404": description: Not Found schema: - $ref: '#/definitions/handlers.errorResponse' + $ref: '#/definitions/handlers.ErrorResponse' "500": description: Internal Server Error schema: - $ref: '#/definitions/handlers.errorResponse' + $ref: '#/definitions/handlers.ErrorResponse' security: - BearerAuth: [] OrgSlugAuth: [] @@ -231,26 +231,26 @@ paths: name: body required: true schema: - $ref: '#/definitions/handlers.updateScheduleRequest' + $ref: '#/definitions/handlers.UpdateScheduleRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/handlers.scheduleResponse' + $ref: '#/definitions/handlers.ScheduleResponse' "400": description: Bad Request schema: - $ref: '#/definitions/handlers.errorResponse' + $ref: '#/definitions/handlers.ErrorResponse' "404": description: Not Found schema: - $ref: '#/definitions/handlers.errorResponse' + $ref: '#/definitions/handlers.ErrorResponse' "500": description: Internal Server Error schema: - $ref: '#/definitions/handlers.errorResponse' + $ref: '#/definitions/handlers.ErrorResponse' security: - BearerAuth: [] OrgSlugAuth: [] @@ -277,12 +277,12 @@ paths: description: OK schema: items: - $ref: '#/definitions/handlers.historyEntry' + $ref: '#/definitions/handlers.HistoryEntry' type: array "500": description: Internal Server Error schema: - $ref: '#/definitions/handlers.errorResponse' + $ref: '#/definitions/handlers.ErrorResponse' security: - BearerAuth: [] OrgSlugAuth: [] @@ -308,15 +308,15 @@ paths: "200": description: OK schema: - $ref: '#/definitions/handlers.runNowResponse' + $ref: '#/definitions/handlers.RunNowResponse' "404": description: Not Found schema: - $ref: '#/definitions/handlers.errorResponse' + $ref: '#/definitions/handlers.ErrorResponse' "500": description: Internal Server Error schema: - $ref: '#/definitions/handlers.errorResponse' + $ref: '#/definitions/handlers.ErrorResponse' security: - BearerAuth: [] OrgSlugAuth: [] diff --git a/workspace-server/internal/handlers/schedules.go b/workspace-server/internal/handlers/schedules.go index 351b2215..e114942d 100644 --- a/workspace-server/internal/handlers/schedules.go +++ b/workspace-server/internal/handlers/schedules.go @@ -15,32 +15,32 @@ import ( "github.com/Molecule-AI/molecule-monorepo/platform/internal/scheduler" ) -// errorResponse is returned for 4xx/5xx errors. (OpenAPI doc shape — used by swaggo.) -type errorResponse struct { +// ErrorResponse is returned for 4xx/5xx errors. (OpenAPI doc shape — used by swaggo.) +type ErrorResponse struct { Error string `json:"error"` } -// statusResponse is returned by mutating endpoints that only echo a status verb. -type statusResponse struct { +// StatusResponse is returned by mutating endpoints that only echo a status verb. +type StatusResponse struct { Status string `json:"status"` } -// createScheduleResponse is returned by POST /workspaces/{id}/schedules. -type createScheduleResponse struct { +// CreateScheduleResponse is returned by POST /workspaces/{id}/schedules. +type CreateScheduleResponse struct { ID string `json:"id"` Status string `json:"status"` NextRunAt time.Time `json:"next_run_at"` } -// runNowResponse is returned by POST /workspaces/{id}/schedules/{scheduleId}/run. -type runNowResponse struct { +// RunNowResponse is returned by POST /workspaces/{id}/schedules/{scheduleId}/run. +type RunNowResponse struct { Status string `json:"status"` WorkspaceID string `json:"workspace_id"` Prompt string `json:"prompt"` } -// historyEntry is one row of /workspaces/{id}/schedules/{scheduleId}/history. -type historyEntry struct { +// HistoryEntry is one row of /workspaces/{id}/schedules/{scheduleId}/history. +type HistoryEntry struct { Timestamp time.Time `json:"timestamp"` DurationMs *int `json:"duration_ms"` Status *string `json:"status"` @@ -54,7 +54,7 @@ func NewScheduleHandler() *ScheduleHandler { return &ScheduleHandler{} } -type scheduleResponse struct { +type ScheduleResponse struct { ID string `json:"id"` WorkspaceID string `json:"workspace_id"` Name string `json:"name"` @@ -78,8 +78,8 @@ type scheduleResponse struct { // @Tags schedules // @Produce json // @Param id path string true "Workspace ID" -// @Success 200 {array} scheduleResponse -// @Failure 500 {object} errorResponse +// @Success 200 {array} ScheduleResponse +// @Failure 500 {object} ErrorResponse // @Router /workspaces/{id}/schedules [get] // @Security BearerAuth && OrgSlugAuth func (h *ScheduleHandler) List(c *gin.Context) { @@ -100,9 +100,9 @@ func (h *ScheduleHandler) List(c *gin.Context) { } defer rows.Close() - schedules := make([]scheduleResponse, 0) + schedules := make([]ScheduleResponse, 0) for rows.Next() { - var s scheduleResponse + var s ScheduleResponse if err := rows.Scan( &s.ID, &s.WorkspaceID, &s.Name, &s.CronExpr, &s.Timezone, &s.Prompt, &s.Enabled, &s.LastRunAt, &s.NextRunAt, &s.RunCount, @@ -120,7 +120,7 @@ func (h *ScheduleHandler) List(c *gin.Context) { c.JSON(http.StatusOK, schedules) } -type createScheduleRequest struct { +type CreateScheduleRequest struct { Name string `json:"name"` CronExpr string `json:"cron_expr" binding:"required"` Timezone string `json:"timezone"` @@ -135,17 +135,17 @@ type createScheduleRequest struct { // @Accept json // @Produce json // @Param id path string true "Workspace ID" -// @Param body body createScheduleRequest true "Schedule fields" -// @Success 201 {object} createScheduleResponse -// @Failure 400 {object} errorResponse -// @Failure 500 {object} errorResponse +// @Param body body CreateScheduleRequest true "Schedule fields" +// @Success 201 {object} CreateScheduleResponse +// @Failure 400 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse // @Router /workspaces/{id}/schedules [post] // @Security BearerAuth && OrgSlugAuth func (h *ScheduleHandler) Create(c *gin.Context) { workspaceID := c.Param("id") ctx := c.Request.Context() - var body createScheduleRequest + var body CreateScheduleRequest if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "cron_expr and prompt are required"}) return @@ -199,7 +199,7 @@ func (h *ScheduleHandler) Create(c *gin.Context) { }) } -type updateScheduleRequest struct { +type UpdateScheduleRequest struct { Name *string `json:"name"` CronExpr *string `json:"cron_expr"` Timezone *string `json:"timezone"` @@ -216,11 +216,11 @@ type updateScheduleRequest struct { // @Produce json // @Param id path string true "Workspace ID" // @Param scheduleId path string true "Schedule ID" -// @Param body body updateScheduleRequest true "Partial schedule fields (only provided keys are updated)" -// @Success 200 {object} scheduleResponse -// @Failure 400 {object} errorResponse -// @Failure 404 {object} errorResponse -// @Failure 500 {object} errorResponse +// @Param body body UpdateScheduleRequest true "Partial schedule fields (only provided keys are updated)" +// @Success 200 {object} ScheduleResponse +// @Failure 400 {object} ErrorResponse +// @Failure 404 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse // @Router /workspaces/{id}/schedules/{scheduleId} [patch] // @Security BearerAuth && OrgSlugAuth func (h *ScheduleHandler) Update(c *gin.Context) { @@ -228,7 +228,7 @@ func (h *ScheduleHandler) Update(c *gin.Context) { workspaceID := c.Param("id") // #113: bind to owning workspace to prevent IDOR ctx := c.Request.Context() - var body updateScheduleRequest + var body UpdateScheduleRequest if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"}) return @@ -304,9 +304,9 @@ func (h *ScheduleHandler) Update(c *gin.Context) { // @Produce json // @Param id path string true "Workspace ID" // @Param scheduleId path string true "Schedule ID" -// @Success 200 {object} statusResponse -// @Failure 404 {object} errorResponse -// @Failure 500 {object} errorResponse +// @Success 200 {object} StatusResponse +// @Failure 404 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse // @Router /workspaces/{id}/schedules/{scheduleId} [delete] // @Security BearerAuth && OrgSlugAuth func (h *ScheduleHandler) Delete(c *gin.Context) { @@ -337,9 +337,9 @@ func (h *ScheduleHandler) Delete(c *gin.Context) { // @Produce json // @Param id path string true "Workspace ID" // @Param scheduleId path string true "Schedule ID" -// @Success 200 {object} runNowResponse -// @Failure 404 {object} errorResponse -// @Failure 500 {object} errorResponse +// @Success 200 {object} RunNowResponse +// @Failure 404 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse // @Router /workspaces/{id}/schedules/{scheduleId}/run [post] // @Security BearerAuth && OrgSlugAuth func (h *ScheduleHandler) RunNow(c *gin.Context) { @@ -378,8 +378,8 @@ func (h *ScheduleHandler) RunNow(c *gin.Context) { // @Produce json // @Param id path string true "Workspace ID" // @Param scheduleId path string true "Schedule ID" -// @Success 200 {array} historyEntry -// @Failure 500 {object} errorResponse +// @Success 200 {array} HistoryEntry +// @Failure 500 {object} ErrorResponse // @Router /workspaces/{id}/schedules/{scheduleId}/history [get] // @Security BearerAuth && OrgSlugAuth func (h *ScheduleHandler) History(c *gin.Context) { @@ -407,9 +407,9 @@ func (h *ScheduleHandler) History(c *gin.Context) { } defer rows.Close() - entries := make([]historyEntry, 0) + entries := make([]HistoryEntry, 0) for rows.Next() { - var e historyEntry + var e HistoryEntry var reqStr string if err := rows.Scan(&e.Timestamp, &e.DurationMs, &e.Status, &e.ErrorDetail, &reqStr); err != nil { continue @@ -421,11 +421,11 @@ func (h *ScheduleHandler) History(c *gin.Context) { c.JSON(http.StatusOK, entries) } -// scheduleHealthResponse is the read-only health view of a schedule. +// ScheduleHealthResponse is the read-only health view of a schedule. // It deliberately omits prompt and cron_expr so sensitive task content is // never exposed to peer workspaces — only execution-state fields needed to // detect silent cron failures are returned (issue #249). -type scheduleHealthResponse struct { +type ScheduleHealthResponse struct { ID string `json:"id"` Name string `json:"name"` Enabled bool `json:"enabled"` @@ -494,9 +494,9 @@ func (h *ScheduleHandler) Health(c *gin.Context) { } defer rows.Close() - schedules := make([]scheduleHealthResponse, 0) + schedules := make([]ScheduleHealthResponse, 0) for rows.Next() { - var s scheduleHealthResponse + var s ScheduleHealthResponse if err := rows.Scan( &s.ID, &s.Name, &s.Enabled, &s.LastRunAt, &s.NextRunAt, &s.RunCount, &s.LastStatus, &s.LastError, diff --git a/workspace-server/internal/handlers/schedules_test.go b/workspace-server/internal/handlers/schedules_test.go index 335a6121..bddcd452 100644 --- a/workspace-server/internal/handlers/schedules_test.go +++ b/workspace-server/internal/handlers/schedules_test.go @@ -234,7 +234,7 @@ func TestScheduleHealth_SelfCall_Allowed(t *testing.T) { t.Fatalf("expected 200 for self-call, got %d: %s", w.Code, w.Body.String()) } - var resp []scheduleHealthResponse + var resp []ScheduleHealthResponse if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to parse response: %v", err) } @@ -284,7 +284,7 @@ func TestScheduleHealth_CanCommunicatePeer_LegacyNoToken(t *testing.T) { t.Fatalf("expected 200 for peer with no tokens, got %d: %s", w.Code, w.Body.String()) } - var resp []scheduleHealthResponse + var resp []ScheduleHealthResponse if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to parse response: %v", err) } -- 2.52.0