From 0d9ee9c44a9a0df12a7769038ad0b7a7884f0ae1 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sun, 22 Feb 2026 23:23:36 -0500 Subject: [PATCH 1/4] security: add algorithm pinning tests for JWT confusion attacks (closes #95) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verified that all JWT validation paths in JWTAuthModule already enforce HS256 via both type assertion (*jwt.SigningMethodHMAC) and explicit algorithm check (token.Method.Alg() != jwt.SigningMethodHS256.Alg()). Added tests to module/jwt_auth_test.go that explicitly confirm tokens signed with HS384 or HS512 are rejected by: - Authenticate() — the AuthProvider interface method - handleRefresh via Handle() — the /auth/refresh endpoint - extractUserFromRequest via Handle() — all protected endpoints The api package (middleware.go, auth_handler.go) already had equivalent algorithm rejection tests in auth_handler_test.go. Co-Authored-By: Claude Opus 4.6 --- module/jwt_auth_test.go | 124 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/module/jwt_auth_test.go b/module/jwt_auth_test.go index 7b737fa4..41fb73fc 100644 --- a/module/jwt_auth_test.go +++ b/module/jwt_auth_test.go @@ -7,6 +7,8 @@ import ( "net/http/httptest" "testing" "time" + + "github.com/golang-jwt/jwt/v5" ) func setupJWTAuth(t *testing.T) *JWTAuthModule { @@ -692,3 +694,125 @@ func TestJWTAuth_UpdateRole_Admin(t *testing.T) { t.Errorf("expected role 'admin', got %v", updated["role"]) } } + +// --- Algorithm confusion / signing method pinning tests --- + +// TestJWTAuth_Authenticate_RejectsNonHS256 verifies that Authenticate() rejects +// tokens signed with algorithms other than HS256 (prevents algorithm confusion attacks). +func TestJWTAuth_Authenticate_RejectsNonHS256(t *testing.T) { + j := setupJWTAuth(t) + + user := &User{ID: "1", Email: "alg@example.com", Name: "Alg Test"} + + for _, method := range []jwt.SigningMethod{jwt.SigningMethodHS384, jwt.SigningMethodHS512} { + method := method + t.Run("rejects "+method.Alg(), func(t *testing.T) { + claims := jwt.MapClaims{ + "sub": user.ID, + "email": user.Email, + "iss": "test-issuer", + "iat": time.Now().Unix(), + "exp": time.Now().Add(24 * time.Hour).Unix(), + } + tok, err := jwt.NewWithClaims(method, claims).SignedString([]byte("test-secret-key")) + if err != nil { + t.Fatalf("failed to sign token with %s: %v", method.Alg(), err) + } + + valid, _, authErr := j.Authenticate(tok) + if authErr != nil { + t.Fatalf("expected nil error, got: %v", authErr) + } + if valid { + t.Errorf("expected token signed with %s to be rejected, but it was accepted", method.Alg()) + } + }) + } +} + +// TestJWTAuth_Authenticate_AcceptsHS256 verifies that valid HS256 tokens are accepted. +func TestJWTAuth_Authenticate_AcceptsHS256(t *testing.T) { + j := setupJWTAuth(t) + + user := &User{ID: "1", Email: "hs256@example.com", Name: "HS256 User"} + tok, err := j.generateToken(user) + if err != nil { + t.Fatalf("failed to generate token: %v", err) + } + + valid, _, err := j.Authenticate(tok) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !valid { + t.Error("expected HS256 token to be valid") + } +} + +// TestJWTAuth_HandleRefresh_RejectsNonHS256 verifies that the refresh handler +// rejects tokens signed with algorithms other than HS256. +func TestJWTAuth_HandleRefresh_RejectsNonHS256(t *testing.T) { + j := setupJWTAuthV1(t) + registerUserV1(t, j, "alg-refresh@example.com", "Alg Refresh", "pass123") + + for _, method := range []jwt.SigningMethod{jwt.SigningMethodHS384, jwt.SigningMethodHS512} { + method := method + t.Run("rejects "+method.Alg(), func(t *testing.T) { + claims := jwt.MapClaims{ + "sub": "1", + "email": "alg-refresh@example.com", + "type": "refresh", + "iss": "test-issuer", + "iat": time.Now().Unix(), + "exp": time.Now().Add(7 * 24 * time.Hour).Unix(), + } + tok, err := jwt.NewWithClaims(method, claims).SignedString([]byte("test-secret-key")) + if err != nil { + t.Fatalf("failed to sign token with %s: %v", method.Alg(), err) + } + + body, _ := json.Marshal(map[string]string{"refresh_token": tok}) + req := httptest.NewRequest(http.MethodPost, "/auth/refresh", bytes.NewReader(body)) + w := httptest.NewRecorder() + j.Handle(w, req) + + if w.Code != http.StatusUnauthorized { + t.Errorf("%s: expected 401, got %d", method.Alg(), w.Code) + } + }) + } +} + +// TestJWTAuth_ExtractUser_RejectsNonHS256 verifies that protected endpoints +// reject Authorization headers containing tokens signed with non-HS256 algorithms. +func TestJWTAuth_ExtractUser_RejectsNonHS256(t *testing.T) { + j := setupJWTAuth(t) + // Register a user so we have a valid user in the store. + registerUser(t, j, "protect@example.com", "Protected User", "pass123") + + for _, method := range []jwt.SigningMethod{jwt.SigningMethodHS384, jwt.SigningMethodHS512} { + method := method + t.Run("profile with "+method.Alg(), func(t *testing.T) { + claims := jwt.MapClaims{ + "sub": "1", + "email": "protect@example.com", + "iss": "test-issuer", + "iat": time.Now().Unix(), + "exp": time.Now().Add(24 * time.Hour).Unix(), + } + tok, err := jwt.NewWithClaims(method, claims).SignedString([]byte("test-secret-key")) + if err != nil { + t.Fatalf("failed to sign token with %s: %v", method.Alg(), err) + } + + req := httptest.NewRequest(http.MethodGet, "/auth/profile", nil) + req.Header.Set("Authorization", "Bearer "+tok) + w := httptest.NewRecorder() + j.Handle(w, req) + + if w.Code != http.StatusUnauthorized { + t.Errorf("%s: expected 401, got %d", method.Alg(), w.Code) + } + }) + } +} From 2cd1e010b7583ea72c7cd12fe9678da3c3a777b5 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sun, 22 Feb 2026 23:36:41 -0500 Subject: [PATCH 2/4] docs: document 10+ undocumented module types in DOCUMENTATION.md (closes #66) Adds detailed documentation for audit logging, license.validator, platform.provider/resource/context, observability.otel, step.jq, AI pipeline steps (ai_complete, ai_classify, ai_extract), CI/CD steps (docker_build, docker_push, docker_run, scan_sast, scan_container, scan_deps, artifact_push, artifact_pull), and the admincore plugin. Each entry includes config field tables with types and defaults, plus a minimal YAML example. Summary tables in the module type index are also updated with the new Infrastructure and CI/CD Step categories. Co-Authored-By: Claude Opus 4.6 --- DOCUMENTATION.md | 566 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 566 insertions(+) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index bf4b154e..806b80cc 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -100,6 +100,22 @@ All modules are registered in `engine.go` and instantiated from YAML config. Org | `step.db_query` | Executes parameterized SQL SELECT queries against a named database | | `step.db_exec` | Executes parameterized SQL INSERT/UPDATE/DELETE against a named database | | `step.json_response` | Writes HTTP JSON response with custom status code and headers | +| `step.jq` | Applies a JQ expression to pipeline data for complex transformations | +| `step.ai_complete` | AI text completion using a configured provider | +| `step.ai_classify` | AI text classification into named categories | +| `step.ai_extract` | AI structured data extraction using tool use or prompt-based parsing | + +### CI/CD Pipeline Steps +| Type | Description | +|------|-------------| +| `step.docker_build` | Builds a Docker image from a context directory and Dockerfile | +| `step.docker_push` | Pushes a Docker image to a remote registry | +| `step.docker_run` | Runs a command inside a Docker container via sandbox | +| `step.scan_sast` | Static Application Security Testing (SAST) via configurable scanner | +| `step.scan_container` | Container image vulnerability scanning via Trivy | +| `step.scan_deps` | Dependency vulnerability scanning via Grype | +| `step.artifact_push` | Stores a file in the artifact store for cross-step sharing | +| `step.artifact_pull` | Retrieves an artifact from a prior execution, URL, or S3 | ### Template Functions @@ -115,6 +131,14 @@ Pipeline steps support Go template syntax with these built-in functions: Template expressions can reference previous step outputs via `{{ .steps.step-name.field }}` or for hyphenated names `{{index .steps "step-name" "field"}}`. +### Infrastructure +| Type | Description | +|------|-------------| +| `license.validator` | License key validation against a remote server with caching and grace period | +| `platform.provider` | Cloud infrastructure provider declaration (e.g., Terraform, Pulumi) | +| `platform.resource` | Infrastructure resource managed by a platform provider | +| `platform.context` | Execution context for platform operations (org, environment, tier) | + ### Observability | Type | Description | |------|-------------| @@ -161,6 +185,548 @@ Template expressions can reference previous step outputs via `{{ .steps.step-nam | `data.transformer` | Data transformation | | `workflow.registry` | Workflow registration and discovery | +## Module Type Reference + +Detailed configuration reference for module types not covered in the main table above. + +### Audit Logging (`audit/`) + +The `audit/` package provides a structured JSON audit logger for recording security-relevant events. It is used internally by the engine and admin platform -- not a YAML module type, but rather a Go library used by other modules. + +**Event types:** `auth`, `auth_failure`, `admin_op`, `escalation`, `data_access`, `config_change`, `component_op` + +Each audit event is written as a single JSON line containing `timestamp`, `type`, `action`, `actor`, `resource`, `detail`, `source_ip`, `success`, and `metadata` fields. + +--- + +### `license.validator` + +Validates license keys against a remote server with local caching and an offline grace period. When no `server_url` is configured the module operates in offline/starter mode and synthesizes a valid starter-tier license locally. + +**Configuration:** + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `server_url` | string | `""` | License validation server URL. Leave empty for offline/starter mode. | +| `license_key` | string | `""` | License key. Supports `$ENV_VAR` expansion. Falls back to `WORKFLOW_LICENSE_KEY` env var. | +| `cache_ttl` | duration | `1h` | How long to cache a valid license result before re-validating. | +| `grace_period` | duration | `72h` | How long to allow operation when the license server is unreachable. | +| `refresh_interval` | duration | `1h` | How often the background goroutine re-validates the license. | + +**Outputs:** Provides the `license-validator` service (`LicenseValidator`). + +**Example:** + +```yaml +modules: + - name: license + type: license.validator + config: + server_url: "https://license.gocodalone.com/api/v1" + license_key: "$WORKFLOW_LICENSE_KEY" + cache_ttl: "1h" + grace_period: "72h" + refresh_interval: "1h" +``` + +--- + +### `platform.provider` + +Declares a cloud infrastructure provider (e.g., Terraform, Pulumi) for use with the platform workflow handler and reconciliation trigger. + +**Configuration:** + +| Key | Type | Required | Description | +|-----|------|----------|-------------| +| `name` | string | yes | Provider name used to construct the service name `platform.provider.`. | + +**Example:** + +```yaml +modules: + - name: cloud-provider + type: platform.provider + config: + name: "aws" +``` + +--- + +### `platform.resource` + +Declares an infrastructure resource managed by a platform provider. Config keys are provider-specific and passed through as-is. + +**Configuration:** + +| Key | Type | Required | Description | +|-----|------|----------|-------------| +| `type` | string | yes | Infrastructure resource type (e.g., `database`, `queue`, `container_runtime`). | +| *(additional keys)* | any | no | Provider-specific resource properties. | + +**Example:** + +```yaml +modules: + - name: orders-db + type: platform.resource + config: + type: database + engine: postgresql + storage: "10Gi" +``` + +--- + +### `platform.context` + +Provides the execution context for platform operations. Used to identify the organization, environment, and tier for a deployment. + +**Configuration:** + +| Key | Type | Required | Description | +|-----|------|----------|-------------| +| `path` | string | yes | Path identifying this context. | +| `org` | string | no | Organization name. | +| `environment` | string | no | Deployment environment (e.g., `production`, `staging`). | +| `tier` | number | no | Platform tier level. | + +**Example:** + +```yaml +modules: + - name: platform-ctx + type: platform.context + config: + path: "acme-corp/production" + org: "acme-corp" + environment: "production" + tier: 3 +``` + +--- + +### `observability.otel` + +Initializes an OpenTelemetry distributed tracing provider that exports spans via OTLP/HTTP to a collector. Sets the global OTel tracer provider so all instrumented code in the process is covered. + +**Configuration:** + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `endpoint` | string | `localhost:4318` | OTLP collector endpoint (host:port). | +| `serviceName` | string | `workflow` | Service name used for trace attribution. | + +**Outputs:** Provides the `tracer` service (`trace.Tracer`). + +**Example:** + +```yaml +modules: + - name: tracing + type: observability.otel + config: + endpoint: "otel-collector:4318" + serviceName: "order-api" +``` + +--- + +### `step.jq` + +Applies a JQ expression to pipeline data for complex transformations. Uses the `gojq` pure-Go JQ implementation, supporting the full JQ language: field access, pipes, `map`/`select`, object construction, arithmetic, conditionals, and more. + +The expression is compiled at startup so syntax errors are caught early. When the result is a single object, its keys are merged into the step output so downstream steps can access fields directly. + +**Configuration:** + +| Key | Type | Required | Description | +|-----|------|----------|-------------| +| `expression` | string | yes | JQ expression to evaluate. | +| `input_from` | string | no | Dotted path to the input value (e.g., `steps.fetch.items`). Defaults to the full current pipeline context. | + +**Output fields:** `result` — the JQ result. When the result is a single object, its keys are also promoted to the top level. + +**Example:** + +```yaml +steps: + - name: extract-active + type: step.jq + config: + input_from: "steps.fetch-users.users" + expression: "[.[] | select(.active == true) | {id, email}]" +``` + +--- + +### `step.ai_complete` + +Invokes an AI provider to produce a text completion. Provider resolution order: explicit `provider` name, then model-based lookup, then first registered provider. + +**Configuration:** + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `provider` | string | `""` | Named AI provider to use. Omit to auto-select. | +| `model` | string | `""` | Model name (e.g., `claude-3-5-sonnet-20241022`). Used for provider lookup if `provider` is unset. | +| `system_prompt` | string | `""` | System prompt. Supports Go template syntax with pipeline context. | +| `input_from` | string | `""` | Template expression to resolve the user message (e.g., `.body`). Falls back to `text` or `body` fields in current context. | +| `max_tokens` | number | `1024` | Maximum tokens in the completion. | +| `temperature` | number | `0` | Sampling temperature (0.0–1.0). | + +**Output fields:** `content`, `model`, `finish_reason`, `usage.input_tokens`, `usage.output_tokens`. + +**Example:** + +```yaml +steps: + - name: summarize + type: step.ai_complete + config: + model: "claude-3-5-haiku-20241022" + system_prompt: "You are a helpful assistant. Summarize the following text concisely." + input_from: ".body" + max_tokens: 512 +``` + +--- + +### `step.ai_classify` + +Classifies input text into one of a configured set of categories using an AI provider. Returns the winning category, a confidence score (0.0–1.0), and brief reasoning. + +**Configuration:** + +| Key | Type | Required | Description | +|-----|------|----------|-------------| +| `categories` | array of strings | yes | List of valid classification categories. | +| `provider` | string | no | Named AI provider. Auto-selected if omitted. | +| `model` | string | no | Model name for provider lookup. | +| `input_from` | string | no | Template expression for the input text. Falls back to `text` or `body` fields. | +| `max_tokens` | number | `256` | Maximum tokens for the classification response. | +| `temperature` | number | `0` | Sampling temperature. | + +**Output fields:** `category`, `confidence`, `reasoning`, `raw`, `model`, `usage.input_tokens`, `usage.output_tokens`. + +**Example:** + +```yaml +steps: + - name: classify-ticket + type: step.ai_classify + config: + input_from: ".body" + categories: + - "billing" + - "technical-support" + - "account" + - "general-inquiry" +``` + +--- + +### `step.ai_extract` + +Extracts structured data from text using an AI provider. When the provider supports tool use, it uses the tool-calling API for reliable structured output. Otherwise it falls back to prompt-based JSON extraction. + +**Configuration:** + +| Key | Type | Required | Description | +|-----|------|----------|-------------| +| `schema` | object | yes | JSON Schema object describing the fields to extract. | +| `provider` | string | no | Named AI provider. Auto-selected if omitted. | +| `model` | string | no | Model name for provider lookup. | +| `input_from` | string | no | Template expression for the input text. Falls back to `text` or `body` fields. | +| `max_tokens` | number | `1024` | Maximum tokens. | +| `temperature` | number | `0` | Sampling temperature. | + +**Output fields:** `extracted` (map of extracted fields), `method` (`tool_use`, `text_parse`, or `prompt`), `model`, `usage.input_tokens`, `usage.output_tokens`. + +**Example:** + +```yaml +steps: + - name: extract-order + type: step.ai_extract + config: + input_from: ".body" + schema: + type: object + properties: + customer_name: {type: string} + order_items: {type: array, items: {type: string}} + total_amount: {type: number} +``` + +--- + +### `step.docker_build` + +Builds a Docker image from a context directory and Dockerfile using the Docker SDK. The context directory is tar-archived and sent to the Docker daemon. + +**Configuration:** + +| Key | Type | Required | Description | +|-----|------|----------|-------------| +| `context` | string | yes | Path to the build context directory. | +| `dockerfile` | string | `Dockerfile` | Dockerfile path relative to the context directory. | +| `tags` | array of strings | no | Image tags to apply (e.g., `["myapp:latest", "myapp:1.2.3"]`). | +| `build_args` | map | no | Build argument key/value pairs. | +| `cache_from` | array of strings | no | Image references to use as layer cache sources. | + +**Output fields:** `image_id`, `tags`, `context`. + +**Example:** + +```yaml +steps: + - name: build-image + type: step.docker_build + config: + context: "./src" + dockerfile: "Dockerfile" + tags: + - "myapp:latest" + build_args: + APP_VERSION: "1.2.3" +``` + +--- + +### `step.docker_push` + +Pushes a Docker image to a remote registry. + +**Configuration:** + +| Key | Type | Required | Description | +|-----|------|----------|-------------| +| `image` | string | yes | Image name/tag to push. | +| `registry` | string | no | Registry hostname prefix (prepended to `image` when constructing the reference). | +| `auth_provider` | string | no | Named auth provider for registry credentials (informational; credentials are read from Docker daemon config). | + +**Output fields:** `image`, `registry`, `digest`, `auth_provider`. + +**Example:** + +```yaml +steps: + - name: push-image + type: step.docker_push + config: + image: "myapp:latest" + registry: "ghcr.io/myorg" +``` + +--- + +### `step.docker_run` + +Runs a command inside a Docker container using the sandbox. Returns exit code, stdout, and stderr. + +**Configuration:** + +| Key | Type | Required | Description | +|-----|------|----------|-------------| +| `image` | string | yes | Docker image to run. | +| `command` | array of strings | no | Command to execute inside the container. Uses image default entrypoint if omitted. | +| `env` | map | no | Environment variables to set in the container. | +| `wait_for_exit` | boolean | `true` | Whether to wait for the container to exit. | +| `timeout` | duration | `""` | Maximum time to wait for the container. | + +**Output fields:** `exit_code`, `stdout`, `stderr`, `image`. + +**Example:** + +```yaml +steps: + - name: run-tests + type: step.docker_run + config: + image: "golang:1.25" + command: ["go", "test", "./..."] + env: + CI: "true" + timeout: "10m" +``` + +--- + +### `step.scan_sast` + +Runs a Static Application Security Testing (SAST) scanner inside a Docker container and evaluates findings against a severity gate. Supports Semgrep and generic scanner commands. + +**Configuration:** + +| Key | Type | Required | Description | +|-----|------|----------|-------------| +| `scanner` | string | yes | Scanner to use. Supported: `semgrep`. Generic commands also accepted. | +| `image` | string | `semgrep/semgrep:latest` | Docker image for the scanner. | +| `source_path` | string | `/workspace` | Path to the source code to scan. | +| `rules` | array of strings | no | Semgrep rule configs to apply (e.g., `auto`, `p/owasp-top-ten`). | +| `fail_on_severity` | string | `error` | Minimum severity that causes the step to fail (`error`, `warning`, `info`). | +| `output_format` | string | `sarif` | Output format: `sarif` or `json`. | + +**Output fields:** `scan_result`, `command`, `image`. + +**Example:** + +```yaml +steps: + - name: sast-scan + type: step.scan_sast + config: + scanner: "semgrep" + source_path: "/workspace/src" + rules: + - "p/owasp-top-ten" + - "p/golang" + fail_on_severity: "error" +``` + +--- + +### `step.scan_container` + +Scans a container image for vulnerabilities using Trivy. Evaluates findings against a configurable severity threshold. + +**Configuration:** + +| Key | Type | Required | Description | +|-----|------|----------|-------------| +| `target_image` | string | yes | Container image to scan (e.g., `myapp:latest`). | +| `scanner` | string | `trivy` | Scanner to use. | +| `severity_threshold` | string | `HIGH` | Minimum severity to report: `CRITICAL`, `HIGH`, `MEDIUM`, `LOW`, or `INFO`. | +| `ignore_unfixed` | boolean | `false` | Skip vulnerabilities without a known fix. | +| `output_format` | string | `sarif` | Output format: `sarif` or `json`. | + +**Output fields:** `scan_result`, `command`, `image`, `target_image`. + +**Example:** + +```yaml +steps: + - name: scan-image + type: step.scan_container + config: + target_image: "myapp:latest" + severity_threshold: "HIGH" + ignore_unfixed: true +``` + +--- + +### `step.scan_deps` + +Scans project dependencies for known vulnerabilities using Grype. Evaluates findings against a severity gate. + +**Configuration:** + +| Key | Type | Required | Description | +|-----|------|----------|-------------| +| `scanner` | string | `grype` | Scanner to use. | +| `image` | string | `anchore/grype:latest` | Docker image for the scanner. | +| `source_path` | string | `/workspace` | Path to the project source to scan. | +| `fail_on_severity` | string | `high` | Minimum severity that causes the step to fail: `critical`, `high`, `medium`, `low`, or `info`. | +| `output_format` | string | `sarif` | Output format: `sarif` or `json`. | + +**Output fields:** `scan_result`, `command`, `image`. + +**Example:** + +```yaml +steps: + - name: dep-scan + type: step.scan_deps + config: + source_path: "/workspace" + fail_on_severity: "high" +``` + +--- + +### `step.artifact_push` + +Reads a file from `source_path` and stores it in the pipeline's artifact store. Computes a SHA-256 checksum of the artifact. Requires `artifact_store` and `execution_id` in pipeline metadata. + +**Configuration:** + +| Key | Type | Required | Description | +|-----|------|----------|-------------| +| `source_path` | string | yes | Path to the file to store. | +| `key` | string | yes | Artifact key under which to store the file. | +| `dest` | string | `artifact_store` | Destination identifier (informational). | + +**Output fields:** `key`, `size`, `checksum`, `dest`. + +**Example:** + +```yaml +steps: + - name: upload-binary + type: step.artifact_push + config: + source_path: "./bin/server" + key: "server-binary" +``` + +--- + +### `step.artifact_pull` + +Retrieves an artifact from a prior execution, a URL, or S3 and writes it to a local destination path. + +**Configuration:** + +| Key | Type | Required | Description | +|-----|------|----------|-------------| +| `source` | string | yes | Source type: `previous_execution`, `url`, or `s3`. | +| `dest` | string | yes | Local file path to write the artifact to. | +| `key` | string | yes (for `previous_execution`, `s3`) | Artifact key to retrieve. | +| `execution_id` | string | no | Specific execution ID to pull from. Defaults to current execution. | +| `url` | string | yes (for `url`) | URL to fetch the artifact from. | + +**Output fields:** `source`, `key`, `dest`, `size`, `bytes_written`. + +**Example:** + +```yaml +steps: + - name: download-binary + type: step.artifact_pull + config: + source: "previous_execution" + key: "server-binary" + dest: "./bin/server" +``` + +--- + +### Admin Core Plugin (`plugin/admincore/`) + +The `admincore` plugin is a NativePlugin that registers the built-in admin UI page definitions. It declares no HTTP routes -- all views are rendered entirely in the React frontend. Registering this plugin ensures navigation is driven by the plugin system with no static fallbacks. + +**UI pages declared:** + +| ID | Label | Category | +|----|-------|----------| +| `dashboard` | Dashboard | global | +| `editor` | Editor | global | +| `marketplace` | Marketplace | global | +| `templates` | Templates | global | +| `environments` | Environments | global | +| `settings` | Settings | global | +| `executions` | Executions | workflow | +| `logs` | Logs | workflow | +| `events` | Events | workflow | + +Global pages appear in the main navigation. Workflow-scoped pages (`executions`, `logs`, `events`) are only shown when a workflow is open. + +The plugin is auto-registered via `init()` in `plugin/admincore/plugin.go`. No YAML configuration is required. + +--- + ## Workflow Types Workflows are configured in YAML and dispatched by the engine through registered handlers (`handlers/` package): From 87af4d0bcfeccc9c9dd6d4dbf84a4f6dfdb82d5f Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 01:22:04 -0500 Subject: [PATCH 3/4] fix: correct documentation inaccuracies for license.validator, platform.*, and observability.otel modules (#109) * Initial plan * fix: correct documentation inaccuracies for license.validator, platform.*, and observability.otel modules Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> --- DOCUMENTATION.md | 38 +++++++++++++++++++------------- plugins/observability/modules.go | 11 +++++++-- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 806b80cc..a5fd61e9 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -208,7 +208,7 @@ Validates license keys against a remote server with local caching and an offline | Key | Type | Default | Description | |-----|------|---------|-------------| | `server_url` | string | `""` | License validation server URL. Leave empty for offline/starter mode. | -| `license_key` | string | `""` | License key. Supports `$ENV_VAR` expansion. Falls back to `WORKFLOW_LICENSE_KEY` env var. | +| `license_key` | string | `""` | License key. When empty, falls back to the `WORKFLOW_LICENSE_KEY` environment variable. | | `cache_ttl` | duration | `1h` | How long to cache a valid license result before re-validating. | | `grace_period` | duration | `72h` | How long to allow operation when the license server is unreachable. | | `refresh_interval` | duration | `1h` | How often the background goroutine re-validates the license. | @@ -223,7 +223,7 @@ modules: type: license.validator config: server_url: "https://license.gocodalone.com/api/v1" - license_key: "$WORKFLOW_LICENSE_KEY" + license_key: "" # leave empty to use WORKFLOW_LICENSE_KEY env var cache_ttl: "1h" grace_period: "72h" refresh_interval: "1h" @@ -233,13 +233,15 @@ modules: ### `platform.provider` -Declares a cloud infrastructure provider (e.g., Terraform, Pulumi) for use with the platform workflow handler and reconciliation trigger. +Declares a cloud infrastructure provider (e.g., AWS, Docker Compose, GCP) for use with the platform workflow handler and reconciliation trigger. **Configuration:** | Key | Type | Required | Description | |-----|------|----------|-------------| -| `name` | string | yes | Provider name used to construct the service name `platform.provider.`. | +| `name` | string | yes | Provider identifier (e.g., `aws`, `docker-compose`, `gcp`). | +| `config` | map[string]string | no | Provider-specific configuration (credentials, region, etc.). | +| `tiers` | JSON | no | Three-tier infrastructure layout (`infrastructure`, `shared_primitives`, `application`). | **Example:** @@ -249,20 +251,25 @@ modules: type: platform.provider config: name: "aws" + config: + region: "us-east-1" ``` --- ### `platform.resource` -Declares an infrastructure resource managed by a platform provider. Config keys are provider-specific and passed through as-is. +A capability-based resource declaration managed by the platform abstraction layer. **Configuration:** | Key | Type | Required | Description | |-----|------|----------|-------------| -| `type` | string | yes | Infrastructure resource type (e.g., `database`, `queue`, `container_runtime`). | -| *(additional keys)* | any | no | Provider-specific resource properties. | +| `name` | string | yes | Unique identifier for this resource within its tier. | +| `type` | string | yes | Abstract capability type (e.g., `container_runtime`, `database`, `message_queue`). | +| `tier` | string | no | Infrastructure tier: `infrastructure`, `shared_primitive`, or `application` (default: `application`). | +| `capabilities` | JSON | no | Provider-agnostic capability properties (replicas, memory, ports, etc.). | +| `constraints` | JSON | no | Hard limits imposed by parent tiers. | **Example:** @@ -271,9 +278,12 @@ modules: - name: orders-db type: platform.resource config: + name: orders-db type: database - engine: postgresql - storage: "10Gi" + tier: application + capabilities: + engine: postgresql + storage: "10Gi" ``` --- @@ -286,10 +296,9 @@ Provides the execution context for platform operations. Used to identify the org | Key | Type | Required | Description | |-----|------|----------|-------------| -| `path` | string | yes | Path identifying this context. | -| `org` | string | no | Organization name. | -| `environment` | string | no | Deployment environment (e.g., `production`, `staging`). | -| `tier` | number | no | Platform tier level. | +| `org` | string | yes | Organization identifier. | +| `environment` | string | yes | Deployment environment (e.g., `production`, `staging`, `dev`). | +| `tier` | string | no | Infrastructure tier: `infrastructure`, `shared_primitive`, or `application` (default: `application`). | **Example:** @@ -298,10 +307,9 @@ modules: - name: platform-ctx type: platform.context config: - path: "acme-corp/production" org: "acme-corp" environment: "production" - tier: 3 + tier: "application" ``` --- diff --git a/plugins/observability/modules.go b/plugins/observability/modules.go index bf8e7704..255b276c 100644 --- a/plugins/observability/modules.go +++ b/plugins/observability/modules.go @@ -82,8 +82,15 @@ func logCollectorFactory(name string, cfg map[string]any) modular.Module { return module.NewLogCollector(name, lcCfg) } -func otelTracingFactory(name string, _ map[string]any) modular.Module { - return module.NewOTelTracing(name) +func otelTracingFactory(name string, cfg map[string]any) modular.Module { + m := module.NewOTelTracing(name) + if v, ok := cfg["endpoint"].(string); ok && v != "" { + m.SetEndpoint(v) + } + if v, ok := cfg["serviceName"].(string); ok && v != "" { + m.SetServiceName(v) + } + return m } func openAPIGeneratorFactory(name string, cfg map[string]any) modular.Module { From bf99a092a4b8c1c110a37d053681e9ee9fed2769 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Mon, 23 Feb 2026 02:40:03 -0500 Subject: [PATCH 4/4] fix: add nil guard for cfg map in otelTracingFactory Prevents potential issues when a module omits the config: section in YAML, which passes a nil map to the factory function. Co-Authored-By: Claude Opus 4.6 --- plugins/observability/modules.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/observability/modules.go b/plugins/observability/modules.go index 255b276c..e7f06649 100644 --- a/plugins/observability/modules.go +++ b/plugins/observability/modules.go @@ -84,6 +84,9 @@ func logCollectorFactory(name string, cfg map[string]any) modular.Module { func otelTracingFactory(name string, cfg map[string]any) modular.Module { m := module.NewOTelTracing(name) + if cfg == nil { + return m + } if v, ok := cfg["endpoint"].(string); ok && v != "" { m.SetEndpoint(v) }