RFC #1706 Phase 1: OpenAPI spec from workspace-server schedules handler (#1707)
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Waiting to run
publish-workspace-server-image / build-and-push (push) Successful in 3m8s
Block internal-flavored paths / Block forbidden paths (push) Successful in 3s
CI / Detect changes (push) Successful in 6s
CI / Python Lint & Test (push) Successful in 5s
E2E API Smoke Test / detect-changes (push) Successful in 9s
E2E Chat / detect-changes (push) Successful in 8s
Handlers Postgres Integration / detect-changes (push) Successful in 5s
Harness Replays / detect-changes (push) Successful in 3s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (push) Successful in 4s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (push) Successful in 3s
Secret scan / Scan diff for credential-shaped strings (push) Successful in 4s
CI / Canvas (Next.js) (push) Successful in 4s
CI / Shellcheck (E2E scripts) (push) Successful in 2s
E2E API Smoke Test / E2E API Smoke Test (push) Successful in 1m54s
Handlers Postgres Integration / Handlers Postgres Integration (push) Successful in 1m57s
Harness Replays / Harness Replays (push) Successful in 2s
CI / Platform (Go) (push) Successful in 4m59s
E2E Chat / E2E Chat (push) Successful in 4m20s
CI / all-required (push) Successful in 7m48s
publish-workspace-server-image / Production auto-deploy (push) Successful in 13m10s
Sweep stale Cloudflare Tunnels / Sweep CF tunnels (push) Successful in 6s
CI / Canvas Deploy Reminder (push) Successful in 3s
Sweep stale e2e-* orgs (staging) / Sweep e2e orgs (push) Successful in 4s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (push) Waiting to run
E2E Staging Canvas (Playwright) / detect-changes (push) Successful in 9s
Staging SaaS smoke (every 30 min) / Staging SaaS smoke (push) Successful in 9m7s
main-red-watchdog / watchdog (push) Successful in 33s
Continuous synthetic E2E (staging) / Synthetic E2E against staging (push) Successful in 10m0s
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (push) Waiting to run
publish-workspace-server-image / build-and-push (push) Successful in 3m8s
Block internal-flavored paths / Block forbidden paths (push) Successful in 3s
CI / Detect changes (push) Successful in 6s
CI / Python Lint & Test (push) Successful in 5s
E2E API Smoke Test / detect-changes (push) Successful in 9s
E2E Chat / detect-changes (push) Successful in 8s
Handlers Postgres Integration / detect-changes (push) Successful in 5s
Harness Replays / detect-changes (push) Successful in 3s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (push) Successful in 4s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (push) Successful in 3s
Secret scan / Scan diff for credential-shaped strings (push) Successful in 4s
CI / Canvas (Next.js) (push) Successful in 4s
CI / Shellcheck (E2E scripts) (push) Successful in 2s
E2E API Smoke Test / E2E API Smoke Test (push) Successful in 1m54s
Handlers Postgres Integration / Handlers Postgres Integration (push) Successful in 1m57s
Harness Replays / Harness Replays (push) Successful in 2s
CI / Platform (Go) (push) Successful in 4m59s
E2E Chat / E2E Chat (push) Successful in 4m20s
CI / all-required (push) Successful in 7m48s
publish-workspace-server-image / Production auto-deploy (push) Successful in 13m10s
Sweep stale Cloudflare Tunnels / Sweep CF tunnels (push) Successful in 6s
CI / Canvas Deploy Reminder (push) Successful in 3s
Sweep stale e2e-* orgs (staging) / Sweep e2e orgs (push) Successful in 4s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (push) Waiting to run
E2E Staging Canvas (Playwright) / detect-changes (push) Successful in 9s
Staging SaaS smoke (every 30 min) / Staging SaaS smoke (push) Successful in 9m7s
main-red-watchdog / watchdog (push) Successful in 33s
Continuous synthetic E2E (staging) / Synthetic E2E against staging (push) Successful in 10m0s
Co-authored-by: hongming-ceo-delegated <hongmingwang@moleculesai.app> Co-committed-by: hongming-ceo-delegated <hongmingwang@moleculesai.app>
This commit was merged in pull request #1707.
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
# use this Makefile; CI calls docker compose / go test directly so the
|
# use this Makefile; CI calls docker compose / go test directly so the
|
||||||
# Makefile can evolve without breaking the build.
|
# 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.
|
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}'
|
@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).
|
# 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).
|
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
|
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)
|
||||||
|
|||||||
@@ -1,3 +1,26 @@
|
|||||||
|
// 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 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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -0,0 +1,521 @@
|
|||||||
|
{
|
||||||
|
"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": [],
|
||||||
|
"OrgSlugAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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": [],
|
||||||
|
"OrgSlugAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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": [],
|
||||||
|
"OrgSlugAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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": [],
|
||||||
|
"OrgSlugAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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": [],
|
||||||
|
"OrgSlugAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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": [],
|
||||||
|
"OrgSlugAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,349 @@
|
|||||||
|
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: []
|
||||||
|
OrgSlugAuth: []
|
||||||
|
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: []
|
||||||
|
OrgSlugAuth: []
|
||||||
|
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: []
|
||||||
|
OrgSlugAuth: []
|
||||||
|
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: []
|
||||||
|
OrgSlugAuth: []
|
||||||
|
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: []
|
||||||
|
OrgSlugAuth: []
|
||||||
|
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: []
|
||||||
|
OrgSlugAuth: []
|
||||||
|
summary: Fire a schedule manually
|
||||||
|
tags:
|
||||||
|
- schedules
|
||||||
|
schemes:
|
||||||
|
- https
|
||||||
|
securityDefinitions:
|
||||||
|
BearerAuth:
|
||||||
|
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"
|
||||||
@@ -15,13 +15,46 @@ import (
|
|||||||
"github.com/Molecule-AI/molecule-monorepo/platform/internal/scheduler"
|
"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{}
|
type ScheduleHandler struct{}
|
||||||
|
|
||||||
func NewScheduleHandler() *ScheduleHandler {
|
func NewScheduleHandler() *ScheduleHandler {
|
||||||
return &ScheduleHandler{}
|
return &ScheduleHandler{}
|
||||||
}
|
}
|
||||||
|
|
||||||
type scheduleResponse struct {
|
type ScheduleResponse struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
WorkspaceID string `json:"workspace_id"`
|
WorkspaceID string `json:"workspace_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -40,6 +73,15 @@ type scheduleResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// List returns all schedules for a workspace.
|
// 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 && OrgSlugAuth
|
||||||
func (h *ScheduleHandler) List(c *gin.Context) {
|
func (h *ScheduleHandler) List(c *gin.Context) {
|
||||||
workspaceID := c.Param("id")
|
workspaceID := c.Param("id")
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
@@ -58,9 +100,9 @@ func (h *ScheduleHandler) List(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
schedules := make([]scheduleResponse, 0)
|
schedules := make([]ScheduleResponse, 0)
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var s scheduleResponse
|
var s ScheduleResponse
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&s.ID, &s.WorkspaceID, &s.Name, &s.CronExpr, &s.Timezone,
|
&s.ID, &s.WorkspaceID, &s.Name, &s.CronExpr, &s.Timezone,
|
||||||
&s.Prompt, &s.Enabled, &s.LastRunAt, &s.NextRunAt, &s.RunCount,
|
&s.Prompt, &s.Enabled, &s.LastRunAt, &s.NextRunAt, &s.RunCount,
|
||||||
@@ -78,7 +120,7 @@ func (h *ScheduleHandler) List(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, schedules)
|
c.JSON(http.StatusOK, schedules)
|
||||||
}
|
}
|
||||||
|
|
||||||
type createScheduleRequest struct {
|
type CreateScheduleRequest struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
CronExpr string `json:"cron_expr" binding:"required"`
|
CronExpr string `json:"cron_expr" binding:"required"`
|
||||||
Timezone string `json:"timezone"`
|
Timezone string `json:"timezone"`
|
||||||
@@ -87,11 +129,23 @@ type createScheduleRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create adds a new schedule for a workspace.
|
// 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 && OrgSlugAuth
|
||||||
func (h *ScheduleHandler) Create(c *gin.Context) {
|
func (h *ScheduleHandler) Create(c *gin.Context) {
|
||||||
workspaceID := c.Param("id")
|
workspaceID := c.Param("id")
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
var body createScheduleRequest
|
var body CreateScheduleRequest
|
||||||
if err := c.ShouldBindJSON(&body); err != nil {
|
if err := c.ShouldBindJSON(&body); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "cron_expr and prompt are required"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "cron_expr and prompt are required"})
|
||||||
return
|
return
|
||||||
@@ -145,7 +199,7 @@ func (h *ScheduleHandler) Create(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type updateScheduleRequest struct {
|
type UpdateScheduleRequest struct {
|
||||||
Name *string `json:"name"`
|
Name *string `json:"name"`
|
||||||
CronExpr *string `json:"cron_expr"`
|
CronExpr *string `json:"cron_expr"`
|
||||||
Timezone *string `json:"timezone"`
|
Timezone *string `json:"timezone"`
|
||||||
@@ -155,12 +209,26 @@ type updateScheduleRequest struct {
|
|||||||
|
|
||||||
// Update modifies a schedule. Uses a fixed UPDATE with COALESCE so only
|
// Update modifies a schedule. Uses a fixed UPDATE with COALESCE so only
|
||||||
// provided fields are changed — no dynamic SQL construction.
|
// 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 && OrgSlugAuth
|
||||||
func (h *ScheduleHandler) Update(c *gin.Context) {
|
func (h *ScheduleHandler) Update(c *gin.Context) {
|
||||||
scheduleID := c.Param("scheduleId")
|
scheduleID := c.Param("scheduleId")
|
||||||
workspaceID := c.Param("id") // #113: bind to owning workspace to prevent IDOR
|
workspaceID := c.Param("id") // #113: bind to owning workspace to prevent IDOR
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
var body updateScheduleRequest
|
var body UpdateScheduleRequest
|
||||||
if err := c.ShouldBindJSON(&body); err != nil {
|
if err := c.ShouldBindJSON(&body); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"})
|
||||||
return
|
return
|
||||||
@@ -230,6 +298,17 @@ func (h *ScheduleHandler) Update(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Delete removes a schedule.
|
// 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 && OrgSlugAuth
|
||||||
func (h *ScheduleHandler) Delete(c *gin.Context) {
|
func (h *ScheduleHandler) Delete(c *gin.Context) {
|
||||||
scheduleID := c.Param("scheduleId")
|
scheduleID := c.Param("scheduleId")
|
||||||
workspaceID := c.Param("id") // #113: bind to owning workspace to prevent IDOR
|
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.
|
// 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 && OrgSlugAuth
|
||||||
func (h *ScheduleHandler) RunNow(c *gin.Context) {
|
func (h *ScheduleHandler) RunNow(c *gin.Context) {
|
||||||
scheduleID := c.Param("scheduleId")
|
scheduleID := c.Param("scheduleId")
|
||||||
workspaceID := c.Param("id")
|
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.
|
// 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 && OrgSlugAuth
|
||||||
func (h *ScheduleHandler) History(c *gin.Context) {
|
func (h *ScheduleHandler) History(c *gin.Context) {
|
||||||
scheduleID := c.Param("scheduleId")
|
scheduleID := c.Param("scheduleId")
|
||||||
workspaceID := c.Param("id")
|
workspaceID := c.Param("id")
|
||||||
@@ -307,17 +407,9 @@ func (h *ScheduleHandler) History(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
type historyEntry struct {
|
entries := make([]HistoryEntry, 0)
|
||||||
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() {
|
for rows.Next() {
|
||||||
var e historyEntry
|
var e HistoryEntry
|
||||||
var reqStr string
|
var reqStr string
|
||||||
if err := rows.Scan(&e.Timestamp, &e.DurationMs, &e.Status, &e.ErrorDetail, &reqStr); err != nil {
|
if err := rows.Scan(&e.Timestamp, &e.DurationMs, &e.Status, &e.ErrorDetail, &reqStr); err != nil {
|
||||||
continue
|
continue
|
||||||
@@ -329,11 +421,11 @@ func (h *ScheduleHandler) History(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, entries)
|
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
|
// It deliberately omits prompt and cron_expr so sensitive task content is
|
||||||
// never exposed to peer workspaces — only execution-state fields needed to
|
// never exposed to peer workspaces — only execution-state fields needed to
|
||||||
// detect silent cron failures are returned (issue #249).
|
// detect silent cron failures are returned (issue #249).
|
||||||
type scheduleHealthResponse struct {
|
type ScheduleHealthResponse struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
@@ -402,9 +494,9 @@ func (h *ScheduleHandler) Health(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
schedules := make([]scheduleHealthResponse, 0)
|
schedules := make([]ScheduleHealthResponse, 0)
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var s scheduleHealthResponse
|
var s ScheduleHealthResponse
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&s.ID, &s.Name, &s.Enabled, &s.LastRunAt, &s.NextRunAt,
|
&s.ID, &s.Name, &s.Enabled, &s.LastRunAt, &s.NextRunAt,
|
||||||
&s.RunCount, &s.LastStatus, &s.LastError,
|
&s.RunCount, &s.LastStatus, &s.LastError,
|
||||||
|
|||||||
@@ -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())
|
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 {
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||||
t.Fatalf("failed to parse response: %v", err)
|
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())
|
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 {
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||||
t.Fatalf("failed to parse response: %v", err)
|
t.Fatalf("failed to parse response: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user