Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
46f95c0
fix: address CodeRabbit review feedback from PR #121
claude Apr 5, 2026
d2ec159
fix: address CodeRabbit review feedback from PR #122
claude Apr 7, 2026
201a67b
fix: address remaining CodeRabbit feedback on PR #129
claude Apr 7, 2026
eeaf3e4
fix: add global security definition to OpenAPI spec (Checkov CKV_OPEN…
claude Apr 7, 2026
79e7cf6
docs: clarify per-arch image tags exist but are for internal manifest…
claude Apr 7, 2026
561c138
fix: add default "hard" to enforcement in SetBudgetRequest OpenAPI spec
claude Apr 9, 2026
fdc1860
fix: add produces/consumes annotations and regenerate OpenAPI spec wi…
claude Apr 9, 2026
f152d0b
fix: improve govulncheck logging and add missing metric docstrings
claude Apr 9, 2026
a5b3e83
fix: emit GHA error annotations for unknown govulncheck vulnerability…
claude Apr 9, 2026
f40246e
fix: use file-level GHA annotation for unfixed vulnerability IDs
claude Apr 9, 2026
e03d987
fix: exclude stdlib crypto/tls and crypto/x509 govulncheck vulns
claude Apr 9, 2026
9c04fc5
fix: also exclude GO-2026-4946 (crypto/x509 policy-validation DoS)
claude Apr 9, 2026
873857a
fix: also exclude GO-2026-4869 (archive/tar unbounded memory)
claude Apr 9, 2026
53ba1ba
fix: exclude GO-2026-4865 (html/template XSS via grpc→net/trace)
claude Apr 9, 2026
02821fc
ci: clarify govulncheck exclusion removal condition
claude Apr 9, 2026
47addb4
fix: restore top-level security, health probe overrides, and addition…
claude Apr 9, 2026
4eff444
build: add spec-patch target to restore OpenAPI fields lost on swag r…
claude Apr 9, 2026
e46c6e8
build: use --ascii-output in jq patch and normalize swagger.yaml form…
claude Apr 9, 2026
5b4297e
fix: add date/date-time format annotations to timestamp fields in Ope…
claude Apr 9, 2026
cb8afb6
fix: use make spec in CI and add --requiredByDefault to Makefile spec…
claude Apr 10, 2026
cae5f7d
fix: sort required arrays in spec-patch to stabilize Spec Freshness CI
michaelmcnees Apr 11, 2026
442973f
fix: align committed swagger.yaml key ordering with make spec output
claude Apr 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 26 additions & 13 deletions .github/workflows/engine-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,30 @@ jobs:
run: go install golang.org/x/vuln/cmd/govulncheck@latest
- name: Run govulncheck
run: |
# GO-2026-4887 and GO-2026-4883 are in docker/docker (testcontainers test dep only).
# Fixed in: N/A — no upstream fix available. Remove exclusions when a patched release ships.
# Known unfixable vulnerabilities — excluded from enforcement:
#
# Indirect test-only dependency (docker/docker via testcontainers); no upstream fix:
# GO-2026-4887, GO-2026-4883
#
# Go standard library vulnerabilities fixed in go1.25.9 / go1.26.2 (released 2026-04-07).
# Reachable in go < 1.25.9 via the listed call chains:
# GO-2026-4865 (html/template: XSS via JS template literals) — grpc→net/trace→html/template
# GO-2026-4869 (archive/tar: unbounded memory in sparse GNU tar archives) — docker/docker→archive/tar
# GO-2026-4870 (crypto/tls: TLS 1.3 key-update deadlock) — EmailSendConnector→crypto/tls
# GO-2026-4946 (crypto/x509: policy-validation DoS) — EmailSendConnector→crypto/tls→crypto/x509
# GO-2026-4947 (crypto/x509: certificate chain-building DoS) — EmailSendConnector→crypto/tls→crypto/x509
# Remove these five exclusions once go-version in this workflow is >= 1.25.9.
OUTPUT=$(govulncheck ./... 2>&1 || true)
echo "$OUTPUT"
ACTIONABLE=$(echo "$OUTPUT" | grep -E '^Vulnerability #[0-9]+:' | grep -vE 'GO-2026-4887|GO-2026-4883' || true)
# Emit each actionable vulnerability ID as a GHA error annotation for visibility.
ALL_IDS=$(echo "$OUTPUT" | grep -oE 'GO-[0-9]+-[0-9]+' | sort -u || true)
echo "govulncheck IDs found: ${ALL_IDS:-none}"
ACTIONABLE_IDS=$(echo "$ALL_IDS" | grep -vE '^(GO-2026-4887|GO-2026-4883|GO-2026-4865|GO-2026-4869|GO-2026-4870|GO-2026-4946|GO-2026-4947)$' || true)
for ID in $ACTIONABLE_IDS; do
echo "::error file=go.mod,line=1,title=Unfixed vulnerability::$ID — add to exclusion list or upgrade the affected dependency"
done
ACTIONABLE=$(echo "$OUTPUT" | grep -E '^Vulnerability #[0-9]+:' | grep -vE 'GO-2026-4887|GO-2026-4883|GO-2026-4865|GO-2026-4869|GO-2026-4870|GO-2026-4946|GO-2026-4947' || true)
if [ -n "$ACTIONABLE" ]; then
echo "FAIL: unfixed actionable vulnerabilities found"
exit 1
fi

Expand All @@ -92,15 +109,11 @@ jobs:
- uses: actions/setup-go@v5
with:
go-version: '1.25'
- name: Install swag
run: go install github.com/swaggo/swag/cmd/swag@v1.16.6
- name: Regenerate spec
- name: Install spec tooling
run: |
swag init \
-g docs.go \
--dir internal/server,internal/workflow,internal/budget \
--output internal/server/docs \
--outputTypes json,yaml \
--parseInternal
go install github.com/swaggo/swag/cmd/swag@v1.16.6
python -m pip install yq
- name: Regenerate spec
run: make spec
- name: Check for drift
run: git diff --exit-code internal/server/docs/
2 changes: 2 additions & 0 deletions .github/workflows/release-engine.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- uses: docker/setup-qemu-action@v3

- uses: docker/setup-buildx-action@v3

- name: Set goreleaser tag
Expand Down
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ ai/

# Internal development process (ephemeral implementation plans)
docs/superpowers/plans/
plans/
marketing/variants/
/plans/
/marketing/variants/

# Node / Bun
node_modules/
Expand Down
4 changes: 2 additions & 2 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ The automated CI path (Version PR → merge) requires no local tooling beyond gi
5b. release-helm.yml → helm push OCI + GitHub Release
```

> **Note on workflow naming:** `release-please.yml` is named after a tool we evaluated but didn't adopt — it actually runs the Changesets CLI. A rename to `changeset-version.yml` is tracked in [the relevant issue].
> **Note on workflow naming:** `release-please.yml` is named after a tool we evaluated but didn't adopt — it actually runs the Changesets CLI. A rename to `changeset-version.yml` is tracked in [dvflw/mantle#123](https://github.com/dvflw/mantle/issues/123).

## Step 1: Add a Changeset (During Development)

Expand Down Expand Up @@ -90,7 +90,7 @@ Those tags trigger the package-specific release workflows:

**Floating tags** (`major.minor`, `major`, `latest`) are only pushed for stable releases. For pre-release versions (e.g. `0.5.0-rc.1`) the versioned image is pushed but floating tags are skipped.

**Platform-specific image tags:** the pipeline no longer publishes per-arch tags like `ghcr.io/dvflw/mantle:<version>-amd64`. Only the multi-arch manifest tag (`ghcr.io/dvflw/mantle:<version>`) is pushed. Update any CI or deploy configs that reference the old suffixed tags.
**Platform-specific image tags:** per-arch tags like `ghcr.io/dvflw/mantle:<version>-amd64` and `ghcr.io/dvflw/mantle:<version>-arm64` exist in GHCR as internal references required by the multi-arch manifest, but are not intended for direct consumption. Always pull the multi-arch manifest tag (`ghcr.io/dvflw/mantle:<version>`), which selects the correct platform automatically. Update any CI or deploy configs that reference the old suffixed tags directly.

**Trivy scan policy:** the `trivy` job runs after `release` with `exit-code: 1` on `CRITICAL` or `HIGH` CVEs. A failure marks the workflow run as failed but does not retract the already-published release. If a CVE scan fails post-release, open a patch release issue immediately.

Expand Down
49 changes: 38 additions & 11 deletions packages/engine/.goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,45 @@ changelog:
order: 999

dockers_v2:
- ids: [mantle]
- image_templates:
- "ghcr.io/dvflw/mantle:{{ .Version }}-amd64"
use: buildx
ids: [mantle]
goos: linux
goarch: amd64
dockerfile: Dockerfile.goreleaser
images:
- "ghcr.io/dvflw/mantle"
tags:
- "{{ .Version }}"
# Floating tags (major.minor, major, latest) are only pushed for stable
# releases. Empty template results are skipped by goreleaser, so these
# evaluate to "" and are dropped for any pre-release version.
- "{{ if not .Prerelease }}{{ .Major }}.{{ .Minor }}{{ end }}"
- "{{ if not .Prerelease }}{{ .Major }}{{ end }}"
- "{{ if not .Prerelease }}latest{{ end }}"
build_flag_templates:
- "--platform=linux/amd64"
- image_templates:
- "ghcr.io/dvflw/mantle:{{ .Version }}-arm64"
use: buildx
ids: [mantle]
goos: linux
goarch: arm64
dockerfile: Dockerfile.goreleaser
build_flag_templates:
- "--platform=linux/arm64"

docker_manifests:
- name_template: "ghcr.io/dvflw/mantle:{{ .Version }}"
image_templates:
- "ghcr.io/dvflw/mantle:{{ .Version }}-amd64"
- "ghcr.io/dvflw/mantle:{{ .Version }}-arm64"
- name_template: "ghcr.io/dvflw/mantle:{{ .Major }}.{{ .Minor }}"
image_templates:
- "ghcr.io/dvflw/mantle:{{ .Version }}-amd64"
- "ghcr.io/dvflw/mantle:{{ .Version }}-arm64"
skip_push: "{{ if or .IsSnapshot .Prerelease }}true{{ end }}"
- name_template: "ghcr.io/dvflw/mantle:{{ .Major }}"
image_templates:
- "ghcr.io/dvflw/mantle:{{ .Version }}-amd64"
- "ghcr.io/dvflw/mantle:{{ .Version }}-arm64"
skip_push: "{{ if or .IsSnapshot .Prerelease }}true{{ end }}"
- name_template: "ghcr.io/dvflw/mantle:latest"
image_templates:
- "ghcr.io/dvflw/mantle:{{ .Version }}-amd64"
- "ghcr.io/dvflw/mantle:{{ .Version }}-arm64"
skip_push: "{{ if or .IsSnapshot .Prerelease }}true{{ end }}"

release:
github:
Expand Down
18 changes: 17 additions & 1 deletion packages/engine/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ LDFLAGS := -X github.com/dvflw/mantle/internal/version.Version=$(VERSION) \
-X github.com/dvflw/mantle/internal/version.Commit=$(COMMIT) \
-X github.com/dvflw/mantle/internal/version.Date=$(DATE)

.PHONY: build test lint clean migrate run dev spec
.PHONY: build test lint clean migrate run dev spec spec-patch

build:
go build -ldflags "$(LDFLAGS)" -o mantle ./cmd/mantle
Expand Down Expand Up @@ -34,4 +34,20 @@ spec: ## Regenerate OpenAPI spec (requires: go install github.com/swaggo/swag/cm
--dir internal/server,internal/workflow,internal/budget \
--output internal/server/docs \
--outputTypes json,yaml \
--requiredByDefault \
--parseInternal
$(MAKE) spec-patch

spec-patch: ## Re-apply manual OpenAPI patches lost on swag regeneration
@# swag init does not emit global security declarations, per-operation security
@# overrides, additionalProperties on free-form objects, or RFC 3339/date format
@# annotations on timestamp fields. Also normalises required[] to alphabetical
@# order (swag emits them in struct field order; we keep them sorted for stable
@# diffs). Patch them back after every regeneration run.
@# Requires jq and yq (pip install yq).
jq --indent 4 --ascii-output '.security=[{"ApiKeyAuth":[]},{"OIDCAuth":[]}]|.paths["/healthz"].get.security=[]|.paths["/readyz"].get.security=[]|.definitions["server.WorkflowDetailResponse"].properties.definition.additionalProperties=true|.definitions["server.UsageResponse"].properties.period_start.format="date"|.definitions["server.ExecutionDetail"].properties.started_at.format="date-time"|.definitions["server.ExecutionDetail"].properties.completed_at.format="date-time"|.definitions["server.ExecutionSummary"].properties.started_at.format="date-time"|.definitions["server.ExecutionSummary"].properties.completed_at.format="date-time"|.definitions["server.StepSummary"].properties.started_at.format="date-time"|.definitions["server.StepSummary"].properties.completed_at.format="date-time"|.definitions["workflow.VersionSummary"].properties.created_at.format="date-time"|.definitions["workflow.WorkflowSummary"].properties.created_at.format="date-time"|.definitions["workflow.WorkflowSummary"].properties.updated_at.format="date-time"|.definitions|=with_entries(.value.required|=if . then sort else . end)' \
internal/server/docs/swagger.json > internal/server/docs/swagger.json.tmp && \
mv internal/server/docs/swagger.json.tmp internal/server/docs/swagger.json
yq -y '.security=[{"ApiKeyAuth":[]},{"OIDCAuth":[]}]|.paths["/healthz"].get.security=[]|.paths["/readyz"].get.security=[]|.definitions["server.WorkflowDetailResponse"].properties.definition.additionalProperties=true|.definitions["server.UsageResponse"].properties.period_start.format="date"|.definitions["server.ExecutionDetail"].properties.started_at.format="date-time"|.definitions["server.ExecutionDetail"].properties.completed_at.format="date-time"|.definitions["server.ExecutionSummary"].properties.started_at.format="date-time"|.definitions["server.ExecutionSummary"].properties.completed_at.format="date-time"|.definitions["server.StepSummary"].properties.started_at.format="date-time"|.definitions["server.StepSummary"].properties.completed_at.format="date-time"|.definitions["workflow.VersionSummary"].properties.created_at.format="date-time"|.definitions["workflow.WorkflowSummary"].properties.created_at.format="date-time"|.definitions["workflow.WorkflowSummary"].properties.updated_at.format="date-time"|.definitions|=with_entries(.value.required|=if . then sort else . end)' \
internal/server/docs/swagger.yaml > internal/server/docs/swagger.yaml.tmp && \
mv internal/server/docs/swagger.yaml.tmp internal/server/docs/swagger.yaml
4 changes: 3 additions & 1 deletion packages/engine/internal/metrics/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,8 @@ var (

// Queue helper functions.

func SetQueueDepth(n int) { QueueDepth.Set(float64(n)) }
// SetQueueDepth sets the Prometheus gauge to the current number of pending steps.
func SetQueueDepth(n int) { QueueDepth.Set(float64(n)) }
// RecordClaimDuration records the duration of a queue claim attempt.
func RecordClaimDuration(d time.Duration) { ClaimDurationSeconds.Observe(d.Seconds()) }

Expand Down Expand Up @@ -207,6 +208,7 @@ var (

// Tool-use helper functions.

// RecordToolCall increments the tool call counter for the given step, tool name, and status.
func RecordToolCall(step, tool, status string) {
ToolCallsTotal.WithLabelValues(step, tool, status).Inc()
}
Expand Down
20 changes: 16 additions & 4 deletions packages/engine/internal/server/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,9 @@ type StepSummary struct {
// handleListExecutions handles GET /api/v1/executions with query param filters.
//
// @Summary List executions
// @Description Returns a paginated list of workflow executions for the authenticated team. Supports filtering by workflow name, status, and age.
// @Description Returns a list of workflow executions for the authenticated team, most recent first. Supports filtering by workflow name, status, and age. Use the limit parameter to cap results (default 20).
// @Tags executions
// @Produce json
// @Param workflow query string false "Filter by workflow name"
// @Param status query string false "Filter by status" Enums(pending,running,completed,failed,cancelled)
// @Param since query string false "Filter by age (e.g. 1h, 7d)"
Expand Down Expand Up @@ -174,6 +175,7 @@ func (s *Server) handleListExecutions(w http.ResponseWriter, r *http.Request) {
// @Summary Get execution detail
// @Description Returns full details of a single execution including all step results.
// @Tags executions
// @Produce json
// @Param id path string true "Execution ID (UUID)"
// @Success 200 {object} ExecutionDetail
// @Failure 400 {object} ErrorResponse
Expand Down Expand Up @@ -317,8 +319,9 @@ type WorkflowListResponse struct {

// WorkflowDetailResponse is returned for GET /api/v1/workflows/{name}.
type WorkflowDetailResponse struct {
Name string `json:"name"`
Version int `json:"version"`
Name string `json:"name"`
Version int `json:"version"`
// Free-form workflow definition as stored (YAML parsed to JSON).
Definition json.RawMessage `json:"definition" swaggertype:"object"`
}

Expand All @@ -345,7 +348,7 @@ type UsageResponse struct {
// SetBudgetRequest is the request body for PUT /api/v1/budgets/{provider}.
type SetBudgetRequest struct {
MonthlyTokenLimit int64 `json:"monthly_token_limit"`
Enforcement string `json:"enforcement"` // "hard" or "warn"
Enforcement string `json:"enforcement,omitempty" default:"hard"` // "hard" or "warn"; defaults to "hard" when omitted
}

// StatusResponse is returned for mutations that produce no data payload.
Expand All @@ -370,6 +373,7 @@ func writeJSONError(w http.ResponseWriter, message string, status int) {
// @Summary List workflow definitions
// @Description Returns all workflow definitions ever applied by the authenticated team.
// @Tags workflows
// @Produce json
// @Success 200 {object} WorkflowListResponse
// @Failure 500 {object} ErrorResponse
// @Security ApiKeyAuth
Expand All @@ -393,6 +397,7 @@ func (s *Server) handleListWorkflows(w http.ResponseWriter, r *http.Request) {
// @Summary Get latest workflow definition
// @Description Returns the latest applied definition for a workflow.
// @Tags workflows
// @Produce json
// @Param name path string true "Workflow name"
// @Success 200 {object} WorkflowDetailResponse
// @Failure 404 {object} ErrorResponse
Expand Down Expand Up @@ -430,6 +435,7 @@ func (s *Server) handleGetWorkflow(w http.ResponseWriter, r *http.Request) {
// @Summary List versions of a workflow
// @Description Returns all historical versions of a workflow in reverse chronological order.
// @Tags workflows
// @Produce json
// @Param name path string true "Workflow name"
// @Success 200 {object} WorkflowVersionListResponse
// @Failure 400 {object} ErrorResponse
Expand Down Expand Up @@ -461,6 +467,7 @@ func (s *Server) handleListWorkflowVersions(w http.ResponseWriter, r *http.Reque
// @Summary Get a specific workflow version
// @Description Returns a specific historical version of a workflow definition.
// @Tags workflows
// @Produce json
// @Param name path string true "Workflow name"
// @Param version path integer true "Version number"
// @Success 200 {object} WorkflowDetailResponse
Expand Down Expand Up @@ -501,6 +508,7 @@ func (s *Server) handleGetWorkflowVersion(w http.ResponseWriter, r *http.Request
// @Summary List provider budgets
// @Description Returns the token budget configuration for all providers configured by the authenticated team.
// @Tags budgets
// @Produce json
// @Success 200 {array} budget.TeamBudget
// @Failure 500 {object} ErrorResponse
// @Security ApiKeyAuth
Expand All @@ -522,6 +530,8 @@ func (s *Server) handleListBudgets(w http.ResponseWriter, r *http.Request) {
// @Summary Set provider budget
// @Description Creates or replaces the monthly token budget for a provider. Enforcement "hard" blocks execution when the limit is reached; "warn" logs a warning only.
// @Tags budgets
// @Accept json
// @Produce json
// @Param provider path string true "Provider name (e.g. openai, bedrock)"
// @Param body body SetBudgetRequest true "Budget configuration"
// @Success 200 {object} StatusResponse
Expand Down Expand Up @@ -579,6 +589,7 @@ func (s *Server) handleSetBudget(w http.ResponseWriter, r *http.Request) {
// @Summary Delete provider budget
// @Description Removes the budget configuration for a provider. Does not affect in-flight executions.
// @Tags budgets
// @Produce json
// @Param provider path string true "Provider name"
// @Success 200 {object} StatusResponse
// @Failure 500 {object} ErrorResponse
Expand Down Expand Up @@ -619,6 +630,7 @@ func (s *Server) handleDeleteBudget(w http.ResponseWriter, r *http.Request) {
// @Summary Get token usage
// @Description Returns token usage aggregated by provider for the current billing period (calendar month UTC).
// @Tags budgets
// @Produce json
// @Param provider query string false "Provider name; omit for total across all providers"
// @Success 200 {object} UsageResponse
// @Failure 500 {object} ErrorResponse
Expand Down
2 changes: 2 additions & 0 deletions packages/engine/internal/server/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type ReadyzResponse struct {
//
// @Summary Liveness probe
// @Tags system
// @Produce json
// @Success 200 {object} HealthResponse
// @Router /healthz [get]
func healthzDoc() {} //nolint:unused,deadcode
Expand All @@ -47,6 +48,7 @@ func healthzDoc() {} //nolint:unused,deadcode
//
// @Summary Readiness probe
// @Tags system
// @Produce json
// @Success 200 {object} ReadyzResponse
// @Failure 503 {object} ReadyzResponse
// @Router /readyz [get]
Expand Down
Loading
Loading