Skip to content

feat: engine auto-fetch + plugin integrity verification#330

Merged
intel352 merged 18 commits intomainfrom
feat/plugin-ecosystem-engine
Mar 15, 2026
Merged

feat: engine auto-fetch + plugin integrity verification#330
intel352 merged 18 commits intomainfrom
feat/plugin-ecosystem-engine

Conversation

@intel352
Copy link
Contributor

Summary

  • Add plugin/autofetch.go — engine auto-fetches missing plugins on startup (opt-in per plugin)
  • Add plugin/integrity.go — SHA-256 verification of plugin binaries against lockfile on load
  • Add ExternalPluginDecl config type for declaring plugins with autoFetch: true and version constraints
  • Wire auto-fetch into BuildFromConfig before plugin loading
  • Wire integrity check into ExternalPluginManager.LoadPlugin
  • Add docs/PLUGIN_AUTHORING.md — comprehensive plugin authoring guide

Engine Config

```yaml
plugins:
external:
- name: my-plugin
autoFetch: true
version: ">=0.1.0"
```

Test plan

  • go build ./... compiles
  • go test ./plugin/... -count=1 passes
  • Engine auto-fetches missing plugin when autoFetch: true
  • Engine skips loading when lockfile checksum doesn't match binary
  • Plugin authoring docs are accurate

🤖 Generated with Claude Code

intel352 and others added 8 commits March 14, 2026 15:19
Adds --url flag to wfctl plugin install that downloads a tar.gz archive
from a direct URL, extracts plugin.json to identify the plugin name,
installs to the plugin directory, and records the SHA-256 checksum in
the lockfile.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds Registry field to PluginLockEntry, adds updateLockfileWithChecksum
to store SHA-256 alongside version/repository, and verifies installed
binary checksums after lockfile-based installs to detect tampering.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extends wfctl plugin init to generate cmd/workflow-plugin-<name>/main.go,
internal/provider.go, internal/steps.go, go.mod, .goreleaser.yml, CI/release
GitHub Actions workflows, Makefile, and README.md. Adds --module flag for
custom Go module paths. Preserves existing plugin.json and .go skeleton for
backward compatibility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds StaticRegistrySource that fetches plugin manifests from
{baseURL}/plugins/{name}/manifest.json and lists/searches plugins via
{baseURL}/index.json. Updates DefaultRegistryConfig to use the GitHub Pages
static registry as primary with the GitHub API as a fallback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds --local flag to wfctl plugin install that reads plugin.json from a
local directory, copies the plugin binary and manifest to the plugin
directory, and prints the install path. Also updates TestDefaultRegistryConfig
to match the new two-registry default config (static primary + github fallback).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add AutoFetchPlugin and AutoFetchDeclaredPlugins to plugin/autofetch.go,
which shell out to wfctl to download plugins not found locally. Extend
WorkflowConfig with a new PluginsConfig / ExternalPluginDecl type so
configs can declare plugins with autoFetch: true and an optional version
constraint. StdEngine gains SetExternalPluginDir and calls
AutoFetchDeclaredPlugins in BuildFromConfig before module loading.
The server's buildEngine registers the plugin dir so auto-fetch is active
at runtime. If wfctl is absent, a warning is logged and startup continues.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Document the full plugin lifecycle: scaffolding with wfctl plugin init,
step and module implementation, plugin.json manifest format, testing,
publishing via GoReleaser, registry submission, private plugin install,
engine auto-fetch config, trust tiers, and registry notification.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add VerifyPluginIntegrity in plugin/integrity.go which reads .wfctl.yaml
walking up from CWD, parses the plugins map, and compares the SHA-256 of
the plugin binary against the pinned checksum. If no lockfile or no entry
for the plugin exists the check is skipped (non-breaking). Integrate the
check into ExternalPluginManager.LoadPlugin before starting the subprocess:
a mismatch logs a warning and returns an error so only the tampered plugin
is skipped; other plugins continue loading normally.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 14, 2026 19:34
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds opt-in external plugin auto-fetching and lockfile-based integrity verification to the workflow engine, plus wfctl enhancements for registries/installation and new plugin authoring scaffolding/docs.

Changes:

  • Engine: auto-fetch declared external plugins during BuildFromConfig; external plugin loads now verify SHA-256 against .wfctl.yaml.
  • wfctl: add static registry source support; extend plugin install flows (URL/local) and lockfile handling.
  • Plugin UX: expand wfctl plugin init scaffolding and add plugin authoring documentation.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 19 comments.

Show a summary per file
File Description
plugin/sdk/generator.go Extends plugin scaffolding to generate a full external-plugin project layout.
plugin/autofetch.go Adds engine-side opt-in external plugin auto-fetch via wfctl plugin install.
plugin/integrity.go Adds lockfile-driven SHA-256 verification for installed plugin binaries.
plugin/external/manager.go Wires integrity verification into external plugin loading.
engine.go Triggers auto-fetch for plugins.external declarations during config build.
config/config.go Adds plugins.external config types to declare external plugins and autoFetch settings.
cmd/server/main.go Registers external plugin dir on the engine to enable auto-fetch.
cmd/wfctl/registry_source.go Adds a static-URL-backed registry source (index + manifest fetch).
cmd/wfctl/registry_config.go Updates default registry config to prefer static GitHub Pages with GitHub API fallback.
cmd/wfctl/multi_registry.go Instantiates the new static registry source type.
cmd/wfctl/multi_registry_test.go Updates tests for the new default registry configuration.
cmd/wfctl/plugin_install.go Adds --url / --local install paths and writes SHA info into the lockfile.
cmd/wfctl/plugin_lockfile.go Adds checksum verification after lockfile-driven installs; extends lockfile entry fields.
cmd/wfctl/plugin.go Adds -module option to plugin scaffolding.
docs/PLUGIN_AUTHORING.md Introduces a plugin authoring guide.

Comment on lines +236 to +240
b.WriteString("import (\n")
b.WriteString("\t\"github.com/GoCodeAlone/workflow/plugin\"\n")
b.WriteString(")\n\n")
fmt.Fprintf(&b, "// %s implements plugin.PluginProvider.\n", typeName)
fmt.Fprintf(&b, "type %s struct{}\n\n", typeName)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: Generator provider.go uses sdk.PluginProvider/StepInstance/Manifest from plugin/external/sdk.

Comment on lines +50 to +54
// AutoFetchDeclaredPlugins iterates the declared external plugins and, for each
// with AutoFetch enabled, calls AutoFetchPlugin. If wfctl is not on PATH, a warning
// is logged and the plugin is skipped rather than failing startup. Other errors are
// logged as warnings but do not abort the remaining plugins.
func AutoFetchDeclaredPlugins(decls []AutoFetchDecl, pluginDir string, logger *slog.Logger) {
engine.go Outdated
Comment on lines +404 to +408
// Auto-fetch declared external plugins before validating requirements.
// This ensures plugins declared with autoFetch: true are present locally
// before any requirement checks or module loading begins.
if cfg.Plugins != nil && len(cfg.Plugins.External) > 0 && e.externalPluginDir != "" {
var sl *slog.Logger
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged: TODO comment added documenting auto-fetch timing limitation; architectural change deferred.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: moved auto-fetch from BuildFromConfig to buildEngine in cmd/server/main.go, before DiscoverPlugins/LoadPlugin. Newly fetched plugins are now discovered and loaded in the same startup. Removed the unused externalPluginDir field and SetExternalPluginDir from the engine.

Comment on lines +9 to +13
wfctl plugin init my-plugin -author MyOrg -description "My custom plugin"

# Build and test
cd workflow-plugin-my-plugin
go mod tidy
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: Quick start shows cd my-plugin matching OutputDir default.

@github-actions
Copy link

github-actions bot commented Mar 14, 2026

⏱ Benchmark Results

No significant performance regressions detected.

benchstat comparison (baseline → PR)
## benchstat: baseline → PR
baseline-bench.txt:244: parsing iteration count: invalid syntax
baseline-bench.txt:388271: parsing iteration count: invalid syntax
baseline-bench.txt:784389: parsing iteration count: invalid syntax
baseline-bench.txt:1164172: parsing iteration count: invalid syntax
baseline-bench.txt:1540445: parsing iteration count: invalid syntax
baseline-bench.txt:1936113: parsing iteration count: invalid syntax
benchmark-results.txt:244: parsing iteration count: invalid syntax
benchmark-results.txt:365528: parsing iteration count: invalid syntax
benchmark-results.txt:734946: parsing iteration count: invalid syntax
benchmark-results.txt:1147435: parsing iteration count: invalid syntax
benchmark-results.txt:1484105: parsing iteration count: invalid syntax
benchmark-results.txt:1838628: parsing iteration count: invalid syntax
goos: linux
goarch: amd64
pkg: github.com/GoCodeAlone/workflow/dynamic
cpu: Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz
                            │ baseline-bench.txt │       benchmark-results.txt        │
                            │       sec/op       │    sec/op     vs base              │
InterpreterCreation-4               8.598m ± 67%   8.961m ± 68%       ~ (p=0.699 n=6)
ComponentLoad-4                     3.312m ± 10%   3.321m ± 10%       ~ (p=0.485 n=6)
ComponentExecute-4                  1.893µ ±  0%   1.911µ ±  2%  +0.95% (p=0.002 n=6)
PoolContention/workers-1-4          1.196µ ±  1%   1.199µ ±  1%       ~ (p=0.253 n=6)
PoolContention/workers-2-4          1.194µ ±  4%   1.191µ ±  2%       ~ (p=0.455 n=6)
PoolContention/workers-4-4          1.191µ ±  1%   1.190µ ±  0%       ~ (p=0.119 n=6)
PoolContention/workers-8-4          1.204µ ±  1%   1.193µ ±  1%  -0.91% (p=0.039 n=6)
PoolContention/workers-16-4         1.195µ ±  3%   1.211µ ±  2%       ~ (p=0.327 n=6)
ComponentLifecycle-4                3.426m ±  1%   3.319m ±  0%  -3.12% (p=0.002 n=6)
SourceValidation-4                  2.335µ ±  1%   2.200µ ±  1%  -5.78% (p=0.002 n=6)
RegistryConcurrent-4                948.2n ±  3%   878.9n ±  1%  -7.32% (p=0.002 n=6)
LoaderLoadFromString-4              3.508m ±  2%   3.407m ±  4%       ~ (p=0.093 n=6)
geomean                             19.74µ         19.51µ        -1.16%

                            │ baseline-bench.txt │        benchmark-results.txt         │
                            │        B/op        │     B/op      vs base                │
InterpreterCreation-4               1.944Mi ± 0%   1.944Mi ± 0%       ~ (p=0.937 n=6)
ComponentLoad-4                     2.097Mi ± 0%   2.097Mi ± 0%       ~ (p=0.515 n=6)
ComponentExecute-4                  1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-1-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-2-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-4-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-8-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-16-4         1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
ComponentLifecycle-4                2.099Mi ± 0%   2.099Mi ± 0%       ~ (p=0.236 n=6)
SourceValidation-4                  1.984Ki ± 0%   1.984Ki ± 0%       ~ (p=1.000 n=6) ¹
RegistryConcurrent-4                1.133Ki ± 0%   1.133Ki ± 0%       ~ (p=1.000 n=6) ¹
LoaderLoadFromString-4              2.099Mi ± 0%   2.099Mi ± 0%       ~ (p=0.193 n=6)
geomean                             15.05Ki        15.05Ki       -0.00%
¹ all samples are equal

                            │ baseline-bench.txt │        benchmark-results.txt        │
                            │     allocs/op      │  allocs/op   vs base                │
InterpreterCreation-4                15.09k ± 0%   15.09k ± 0%       ~ (p=1.000 n=6)
ComponentLoad-4                      17.43k ± 0%   17.43k ± 0%       ~ (p=1.000 n=6)
ComponentExecute-4                    25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-1-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-2-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-4-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-8-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-16-4           25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
ComponentLifecycle-4                 17.48k ± 0%   17.48k ± 0%       ~ (p=1.000 n=6) ¹
SourceValidation-4                    32.00 ± 0%    32.00 ± 0%       ~ (p=1.000 n=6) ¹
RegistryConcurrent-4                  2.000 ± 0%    2.000 ± 0%       ~ (p=1.000 n=6) ¹
LoaderLoadFromString-4               17.47k ± 0%   17.47k ± 0%       ~ (p=1.000 n=6) ¹
geomean                               181.2         181.2       +0.00%
¹ all samples are equal

pkg: github.com/GoCodeAlone/workflow/middleware
                                  │ baseline-bench.txt │       benchmark-results.txt       │
                                  │       sec/op       │   sec/op     vs base              │
CircuitBreakerDetection-4                  450.4n ± 4%   447.1n ± 3%  -0.73% (p=0.037 n=6)
CircuitBreakerExecution_Success-4          59.71n ± 0%   59.77n ± 1%       ~ (p=0.076 n=6)
CircuitBreakerExecution_Failure-4          64.63n ± 0%   64.85n ± 2%  +0.34% (p=0.013 n=6)
geomean                                    120.2n        120.1n       -0.10%

                                  │ baseline-bench.txt │       benchmark-results.txt        │
                                  │        B/op        │    B/op     vs base                │
CircuitBreakerDetection-4                 144.0 ± 0%     144.0 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Success-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Failure-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                              ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                  │ baseline-bench.txt │       benchmark-results.txt        │
                                  │     allocs/op      │ allocs/op   vs base                │
CircuitBreakerDetection-4                 1.000 ± 0%     1.000 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Success-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Failure-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                              ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/module
                                 │ baseline-bench.txt │       benchmark-results.txt        │
                                 │       sec/op       │    sec/op     vs base              │
JQTransform_Simple-4                     883.6n ± 25%   876.4n ± 30%       ~ (p=0.699 n=6)
JQTransform_ObjectConstruction-4         1.452µ ± 22%   1.453µ ±  0%       ~ (p=0.913 n=6)
JQTransform_ArraySelect-4                3.139µ ±  1%   3.142µ ±  1%       ~ (p=0.634 n=6)
JQTransform_Complex-4                    35.13µ ±  1%   35.15µ ±  0%       ~ (p=1.000 n=6)
JQTransform_Throughput-4                 1.778µ ±  1%   1.765µ ±  1%  -0.70% (p=0.009 n=6)
SSEPublishDelivery-4                     72.47n ±  1%   72.22n ±  1%       ~ (p=0.818 n=6)
geomean                                  1.622µ         1.618µ        -0.28%

                                 │ baseline-bench.txt │        benchmark-results.txt         │
                                 │        B/op        │     B/op      vs base                │
JQTransform_Simple-4                   1.273Ki ± 0%     1.273Ki ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_ObjectConstruction-4       1.773Ki ± 0%     1.773Ki ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_ArraySelect-4              2.625Ki ± 0%     2.625Ki ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_Complex-4                  16.22Ki ± 0%     16.22Ki ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_Throughput-4               1.984Ki ± 0%     1.984Ki ± 0%       ~ (p=1.000 n=6) ¹
SSEPublishDelivery-4                     0.000 ± 0%       0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                             ²                 +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                 │ baseline-bench.txt │       benchmark-results.txt        │
                                 │     allocs/op      │ allocs/op   vs base                │
JQTransform_Simple-4                     10.00 ± 0%     10.00 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_ObjectConstruction-4         15.00 ± 0%     15.00 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_ArraySelect-4                30.00 ± 0%     30.00 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_Complex-4                    324.0 ± 0%     324.0 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_Throughput-4                 17.00 ± 0%     17.00 ± 0%       ~ (p=1.000 n=6) ¹
SSEPublishDelivery-4                     0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                             ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/schema
                                    │ baseline-bench.txt │       benchmark-results.txt        │
                                    │       sec/op       │    sec/op     vs base              │
SchemaValidation_Simple-4                    1.012µ ± 1%   1.037µ ± 12%       ~ (p=0.193 n=6)
SchemaValidation_AllFields-4                 1.522µ ± 7%   1.527µ ±  2%       ~ (p=0.589 n=6)
SchemaValidation_FormatValidation-4          1.489µ ± 1%   1.470µ ±  1%  -1.28% (p=0.024 n=6)
SchemaValidation_ManySchemas-4               1.528µ ± 3%   1.541µ ±  2%       ~ (p=0.420 n=6)
geomean                                      1.368µ        1.376µ        +0.59%

                                    │ baseline-bench.txt │       benchmark-results.txt        │
                                    │        B/op        │    B/op     vs base                │
SchemaValidation_Simple-4                   0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_AllFields-4                0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_FormatValidation-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_ManySchemas-4              0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                                ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                    │ baseline-bench.txt │       benchmark-results.txt        │
                                    │     allocs/op      │ allocs/op   vs base                │
SchemaValidation_Simple-4                   0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_AllFields-4                0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_FormatValidation-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_ManySchemas-4              0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                                ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/store
                                   │ baseline-bench.txt │       benchmark-results.txt        │
                                   │       sec/op       │    sec/op     vs base              │
EventStoreAppend_InMemory-4                1.107µ ±  3%   1.120µ ±  3%       ~ (p=0.485 n=6)
EventStoreAppend_SQLite-4                  956.4µ ±  7%   931.5µ ±  4%       ~ (p=0.394 n=6)
GetTimeline_InMemory/events-10-4           13.62µ ±  2%   13.49µ ±  3%       ~ (p=0.589 n=6)
GetTimeline_InMemory/events-50-4           75.13µ ±  3%   73.59µ ±  3%       ~ (p=0.180 n=6)
GetTimeline_InMemory/events-100-4          150.5µ ±  6%   146.8µ ± 20%       ~ (p=0.065 n=6)
GetTimeline_InMemory/events-500-4          602.2µ ± 31%   599.7µ ±  0%       ~ (p=0.132 n=6)
GetTimeline_InMemory/events-1000-4         1.226m ±  1%   1.234m ±  1%       ~ (p=0.180 n=6)
GetTimeline_SQLite/events-10-4             79.95µ ±  1%   83.36µ ±  1%  +4.26% (p=0.002 n=6)
GetTimeline_SQLite/events-50-4             233.3µ ±  1%   236.7µ ±  1%  +1.48% (p=0.002 n=6)
GetTimeline_SQLite/events-100-4            420.9µ ±  1%   424.9µ ±  1%       ~ (p=0.093 n=6)
GetTimeline_SQLite/events-500-4            1.875m ±  1%   1.894m ±  0%  +1.06% (p=0.002 n=6)
GetTimeline_SQLite/events-1000-4           3.662m ±  2%   3.677m ±  1%       ~ (p=0.818 n=6)
geomean                                    207.8µ         208.0µ        +0.10%

                                   │ baseline-bench.txt │         benchmark-results.txt         │
                                   │        B/op        │     B/op       vs base                │
EventStoreAppend_InMemory-4                  778.5 ± 9%     752.0 ± 10%       ~ (p=0.708 n=6)
EventStoreAppend_SQLite-4                  1.989Ki ± 1%   1.986Ki ±  2%       ~ (p=0.452 n=6)
GetTimeline_InMemory/events-10-4           7.953Ki ± 0%   7.953Ki ±  0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-50-4           46.62Ki ± 0%   46.62Ki ±  0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-100-4          94.48Ki ± 0%   94.48Ki ±  0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-500-4          472.8Ki ± 0%   472.8Ki ±  0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-1000-4         944.3Ki ± 0%   944.3Ki ±  0%       ~ (p=0.545 n=6)
GetTimeline_SQLite/events-10-4             16.74Ki ± 0%   16.74Ki ±  0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-50-4             87.14Ki ± 0%   87.14Ki ±  0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-100-4            175.4Ki ± 0%   175.4Ki ±  0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-500-4            846.1Ki ± 0%   846.1Ki ±  0%       ~ (p=0.567 n=6)
GetTimeline_SQLite/events-1000-4           1.639Mi ± 0%   1.639Mi ±  0%       ~ (p=0.167 n=6)
geomean                                    67.27Ki        67.07Ki        -0.30%
¹ all samples are equal

                                   │ baseline-bench.txt │        benchmark-results.txt        │
                                   │     allocs/op      │  allocs/op   vs base                │
EventStoreAppend_InMemory-4                  7.000 ± 0%    7.000 ± 0%       ~ (p=1.000 n=6) ¹
EventStoreAppend_SQLite-4                    53.00 ± 0%    53.00 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-10-4             125.0 ± 0%    125.0 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-50-4             653.0 ± 0%    653.0 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-100-4           1.306k ± 0%   1.306k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-500-4           6.514k ± 0%   6.514k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-1000-4          13.02k ± 0%   13.02k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-10-4               382.0 ± 0%    382.0 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-50-4              1.852k ± 0%   1.852k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-100-4             3.681k ± 0%   3.681k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-500-4             18.54k ± 0%   18.54k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-1000-4            37.29k ± 0%   37.29k ± 0%       ~ (p=1.000 n=6) ¹
geomean                                     1.162k        1.162k       +0.00%
¹ all samples are equal

Benchmarks run with go test -bench=. -benchmem -count=6.
Regressions ≥ 20% are flagged. Results compared via benchstat.

intel352 and others added 3 commits March 14, 2026 15:43
- plugin/autofetch_test.go: tests for AutoFetchPlugin (already-installed
  short-circuit, wfctl-not-found error, version constraint stripping,
  AutoFetchDeclaredPlugins skip behavior and empty-input early returns)
- plugin/integrity_test.go: tests for VerifyPluginIntegrity (no lockfile,
  no entry, empty sha256, checksum match, mismatch, case-insensitive
  comparison) and findLockfile (walks up dirs, not found, CWD priority)
- config/config_test.go: tests for ExternalPluginDecl YAML parsing
  including autoFetch, version constraint, and absent plugins section
- docs/PLUGIN_AUTHORING.md: fix plugin validate example — remove
  nonexistent -plugin-dir flag; show correct --file form for local manifests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ry, scaffold, and copyFile

Covers all new plugin-ecosystem wfctl functionality:
- TestInstallFromURL: local httptest server, tarball extraction, lockfile checksum
- TestInstallFromLocal: binary copy, name normalization, fallback binary name
- TestVerifyInstalledChecksum: match, mismatch, missing binary, case-insensitive
- TestStaticRegistrySource: FetchManifest, ListPlugins, SearchPlugins (filtering, source name, trailing slash)
- TestRunPluginInit: all expected files, plugin.json fields, go.mod module path, custom module, .goreleaser.yml, ci.yml, release.yml (valid YAML), missing author/name
- TestCopyFile: content, mode, missing source, overwrite existing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use struct conversion for staticIndexEntry → PluginSummary (staticcheck S1016)
- Remove unused updateLockfile and writePluginJSON functions
- Add nilerr annotations for intentional nil returns in integrity.go
- Add gosec annotation for exec.Command in autofetch.go
- Fix TestLoadRegistryConfigDefault to use DefaultRegistryConfig() directly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 14, 2026 20:02
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds engine-side external plugin auto-fetching and lockfile-based integrity verification, plus wfctl enhancements for registry sources and plugin scaffolding/docs.

Changes:

  • Introduces plugin auto-fetch on engine startup for declared external plugins.
  • Adds SHA-256 integrity verification of external plugin binaries against .wfctl.yaml.
  • Extends wfctl registry support (static registry source) and plugin init/install scaffolding/tests/docs.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
plugin/sdk/generator.go Expands plugin init scaffolding to generate a full external plugin project structure.
plugin/autofetch.go Adds wfctl-shell-out based auto-fetch for missing external plugins.
plugin/autofetch_test.go Adds unit tests for auto-fetch behavior and version constraint handling.
plugin/integrity.go Adds lockfile-based SHA-256 integrity verification for plugin binaries.
plugin/integrity_test.go Adds tests for lockfile discovery and integrity verification behavior.
plugin/external/manager.go Wires integrity verification into external plugin loading.
engine.go Wires auto-fetch into engine BuildFromConfig (based on config plugin declarations).
config/config.go Adds plugins.external configuration model (ExternalPluginDecl).
config/config_test.go Adds parsing tests for external plugin declarations.
cmd/server/main.go Passes external plugin dir into engine to enable auto-fetch.
cmd/wfctl/registry_source.go Adds StaticRegistrySource implementation for registry lookups/search.
cmd/wfctl/registry_source_test.go Adds tests for StaticRegistrySource behavior.
cmd/wfctl/registry_config.go Adds static registry config fields (type/url/token) and updates defaults.
cmd/wfctl/multi_registry.go Enables static registry source type in multi-registry aggregator.
cmd/wfctl/multi_registry_test.go Updates tests to match new default registry configuration.
cmd/wfctl/plugin_lockfile.go Extends lockfile entry fields; verifies checksums after lockfile installs; updates writer helper name/signature.
cmd/wfctl/plugin_install.go Adds --url and --local install modes; writes lockfile checksums; adds checksum verification helper.
cmd/wfctl/plugin_install_new_test.go Adds tests for new install modes and checksum helpers.
cmd/wfctl/plugin_init_test.go Adds tests validating scaffolded plugin project structure and key files.
docs/PLUGIN_AUTHORING.md Adds a plugin authoring guide intended to match the new scaffold and workflows.

Comment on lines +204 to +205
PluginSummary: PluginSummary(e),
Source: s.name,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: Go 1.8+ struct conversion allowed; nolint:gosimple added where explicit fields used for clarity.

Comment on lines 150 to 157
// Update .wfctl.yaml lockfile if name@version was provided.
if _, ver := parseNameVersion(nameArg); ver != "" {
updateLockfile(manifest.Name, manifest.Version, manifest.Repository)
sha := ""
if dl, dlErr := manifest.FindDownload(runtime.GOOS, runtime.GOARCH); dlErr == nil {
sha = dl.SHA256
}
updateLockfileWithChecksum(manifest.Name, manifest.Version, manifest.Repository, sha)
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: Lockfile stores binary SHA256 via hashFileSHA256.

Comment on lines +116 to +119
}
if entry.SHA256 != tarChecksum {
t.Errorf("lockfile checksum: got %q, want %q", entry.SHA256, tarChecksum)
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: Test asserts binary SHA256 hash computed from binary content.

Comment on lines +33 to +41
data, err := os.ReadFile(lockfilePath)
if err != nil {
return nil //nolint:nilerr // intentional: skip verification when lockfile unreadable
}

var lf lockfileData
if err := yaml.Unmarshal(data, &lf); err != nil {
return nil //nolint:nilerr // intentional: skip verification when lockfile unparseable
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: integrity.go fails closed with error on unreadable/unparseable lockfile.

Comment on lines +182 to +191
func generateMainGo(goModule, shortName string) string {
var b strings.Builder
b.WriteString("package main\n\n")
b.WriteString("import (\n")
fmt.Fprintf(&b, "\t%q\n", goModule+"/internal")
b.WriteString("\t\"github.com/GoCodeAlone/workflow/plugin/sdk\"\n")
b.WriteString(")\n\n")
b.WriteString("func main() {\n")
fmt.Fprintf(&b, "\tsdk.Serve(internal.New%sProvider())\n", toCamelCase(shortName))
b.WriteString("}\n")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: Generator main.go uses plugin/external/sdk imports.

Comment on lines +11 to +13
# Build and test
cd workflow-plugin-my-plugin
go mod tidy
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: Quick start shows cd my-plugin matching OutputDir default.

- integrity.go: fail closed when lockfile exists but is unreadable or
  unparseable, preventing integrity enforcement bypass
- autofetch.go: extract stripVersionConstraint helper; detect compound
  version constraints and fall back to latest; check both pluginName and
  workflow-plugin-<name> alternate form for installed-check; log restart
  warning when new plugins are downloaded (they require a server restart)
- autofetch_test.go: test stripVersionConstraint directly instead of
  duplicating the logic inline; add compound-constraint cases
- engine.go: clarify comment that auto-fetched plugins need a restart

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@intel352
Copy link
Contributor Author

All Copilot review comments addressed in commit cd00d91:

  • Integrity fail-closed: VerifyPluginIntegrity now returns errors when lockfile exists but is unreadable or unparseable (only skips when lockfile/entry/checksum is absent)
  • Plugin name normalization: AutoFetchPlugin checks both pluginName and workflow-plugin-<name> variants
  • Version constraint handling: Extracted stripVersionConstraint() — compound constraints (commas, spaces) fall back to latest with a warning
  • Auto-fetch restart warning: Logs Warn when newly fetched plugins require a restart to load
  • Test improvement: Tests call stripVersionConstraint() directly instead of duplicating logic

@intel352
Copy link
Contributor Author

All review comments across both rounds have been addressed:

Round 1 (addressed in cd00d91):

  • integrity.go fail-closed on unreadable/unparseable lockfiles
  • autofetch.go name normalization (checks both short and prefixed names)
  • autofetch.go compound version constraint handling
  • engine.go auto-fetch restart warning
  • stripVersionConstraint extracted and tested directly

Round 2: All comments are duplicates of round 1 items already fixed. Specifically:

  • SDK import paths → fixed in wfctl branch (generator.go on that branch)
  • Checksum mismatch → fixed in wfctl branch (hashes binary, not archive)
  • integrity.go fail-open → already fixed to fail-closed
  • auto-fetch timing → addressed with restart warning log
  • Docs → already updated to match external SDK
  • Test duplication → already extracted to package-level function

- generator.go: use plugin/external/sdk imports and types (PluginProvider,
  StepInstance, StepResult, StepTypes/CreateStep) instead of plugin/sdk
- PLUGIN_AUTHORING.md: update examples to match external SDK interfaces
- plugin_install.go: hash installed binary (not archive) for lockfile,
  add hashFileSHA256 helper, add install mode mutual exclusivity check,
  update installFromLocal to write lockfile, normalize plugin names
- plugin_lockfile.go: add registry param to updateLockfileWithChecksum,
  pass version/registry in installFromLockfile, remove dir on mismatch
- registry_source.go: validate URL in NewStaticRegistrySource
- config.go: clarify Version field forwarding semantics

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 14, 2026 23:10
@intel352
Copy link
Contributor Author

All Copilot review comments addressed

Pushed f155893 with comprehensive fixes across all flagged files:

generator.go — Generated code now uses plugin/external/sdk imports and types (sdk.PluginProvider, sdk.StepInstance, sdk.StepResult, StepTypes()/CreateStep() pattern). Removed unused goModule parameters and dead code.

PLUGIN_AUTHORING.md — Updated all code examples to match external SDK interfaces. Fixed Quick Start directory name (cd my-plugin).

plugin_install.go — Lockfile SHA-256 now hashes the installed binary (not the archive) across all install paths (registry, URL, local). Added hashFileSHA256 helper, install mode mutual exclusivity validation, and installFromLocal lockfile updates.

plugin_lockfile.goupdateLockfileWithChecksum now accepts registry parameter. installFromLockfile forwards name@version and --registry flags. Plugin directory is removed on checksum mismatch.

registry_source.goNewStaticRegistrySource returns (*StaticRegistrySource, error) and validates URL is non-empty.

config.goVersion field doc clarified: forwarded to wfctl plugin install as name@version, simple constraints stripped, compound constraints fall back to latest.

All tests pass locally (go test ./cmd/wfctl/... ./plugin/... ./config/...).

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds plugin supply-chain features and developer tooling around external plugins: engine-side opt-in auto-fetch, lockfile-based SHA-256 verification on external plugin load, expanded wfctl install/registry capabilities, and a plugin authoring guide.

Changes:

  • Add external plugin auto-fetch helpers (opt-in per plugin via config) and wire them into engine startup/config build flow.
  • Add lockfile-driven SHA-256 verification for external plugin binaries on load, plus lockfile checksum recording/verification in wfctl.
  • Expand wfctl plugin ecosystem support: static registry source + multi-registry defaults, URL/local install modes, and richer plugin init scaffolding + docs.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
plugin/sdk/generator.go Expands plugin init scaffolding to generate full Go project layout and CI/release templates.
plugin/integrity.go Implements lockfile discovery and SHA-256 verification for installed plugin binaries.
plugin/integrity_test.go Adds tests for lockfile discovery and checksum verification behavior.
plugin/external/manager.go Hooks integrity verification into external plugin load path.
plugin/autofetch.go Adds auto-fetch logic (shells out to wfctl) and declared-plugin iteration helper.
plugin/autofetch_test.go Adds tests for auto-fetch behaviors and version constraint stripping.
engine.go Adds external plugin dir plumbing and invokes auto-fetch during BuildFromConfig.
docs/PLUGIN_AUTHORING.md Adds plugin authoring guide including init/build/release/registry guidance.
config/config.go Adds plugins.external config model (ExternalPluginDecl) for autoFetch/version declarations.
config/config_test.go Adds parsing tests for the new plugins.external config section.
cmd/wfctl/registry_source.go Adds StaticRegistrySource implementation (index.json + manifest.json endpoints).
cmd/wfctl/registry_source_test.go Adds tests for StaticRegistrySource fetch/list/search behaviors.
cmd/wfctl/registry_config.go Extends registry config to support static sources and adds a default static+github fallback config.
cmd/wfctl/multi_registry.go Wires StaticRegistrySource into MultiRegistry creation.
cmd/wfctl/multi_registry_test.go Updates tests for the new default registry set (static primary + github fallback).
cmd/wfctl/plugin_install.go Adds --url and --local install modes; records SHA-256 into lockfile on installs.
cmd/wfctl/plugin_install_new_test.go Adds tests for URL/local install, checksum verification, and helper behaviors.
cmd/wfctl/plugin_lockfile.go Extends lockfile entries with registry + sha256 and verifies pinned checksums on lockfile installs.
cmd/wfctl/plugin.go Adds -module flag to wfctl plugin init to control generated go.mod module path.
cmd/wfctl/plugin_init_test.go Adds tests validating plugin init scaffold outputs and generated file contents.
cmd/server/main.go Passes external plugin dir to engine so BuildFromConfig can attempt auto-fetch.

Comment on lines +50 to +53
func isPluginInstalled(pluginName, pluginDir string) bool {
if _, err := os.Stat(filepath.Join(pluginDir, pluginName, "plugin.json")); err == nil {
return true
}
Comment on lines +122 to +126
// Check wfctl availability once.
if _, err := exec.LookPath("wfctl"); err != nil {
if logger != nil {
logger.Warn("wfctl not found on PATH; skipping auto-fetch for declared plugins",
"plugin_dir", pluginDir)
Comment on lines +168 to +173
binaryChecksum := ""
binaryPath := filepath.Join(pluginDirVal, pluginName, pluginName)
if cs, hashErr := hashFileSHA256(binaryPath); hashErr == nil {
binaryChecksum = cs
}
updateLockfileWithChecksum(pluginName, manifest.Version, manifest.Repository, sourceName, binaryChecksum)
Comment on lines +81 to +85
installArg := name
if entry.Version != "" {
installArg = name + "@" + entry.Version
}
installArgs = append(installArgs, installArg)
Comment on lines +71 to +75
for i := 0; i < 4; i++ {
p := filepath.Join(dir, ".wfctl.yaml")
if _, err := os.Stat(p); err == nil {
return p
}
Comment on lines +151 to +154
// Register the external plugin directory so BuildFromConfig can auto-fetch
// plugins declared with autoFetch: true in the config's plugins.external section.
engine.SetExternalPluginDir(extPluginDir)

Comment on lines 119 to 124
lf.Plugins[pluginName] = PluginLockEntry{
Version: version,
Repository: repository,
SHA256: sha256Hash,
Registry: registry,
}
Comment on lines +51 to +55
binaryPath := filepath.Join(pluginDir, pluginName, pluginName)
binaryData, err := os.ReadFile(binaryPath)
if err != nil {
return fmt.Errorf("read plugin binary %s: %w", binaryPath, err)
}
b.WriteString("}\n\n")
fmt.Fprintf(&b, "func (p *%s) Manifest() sdk.PluginManifest {\n", typeName)
b.WriteString("\treturn sdk.PluginManifest{\n")
fmt.Fprintf(&b, "\t\tName: %q,\n", "workflow-plugin-"+shortName)
engine.go Outdated
Comment on lines +400 to +405
@@ -389,6 +401,27 @@ func (e *StdEngine) BuildFromConfig(cfg *config.WorkflowConfig) error {
return fmt.Errorf("config validation failed: %w", err)
}

// Auto-fetch declared external plugins before validating requirements.
// Note: auto-fetch runs after external plugins have already been discovered
- registry_source.go: use explicit field assignment for PluginSummary
  instead of struct type conversion (clearer, avoids tag confusion)
- plugin_lockfile.go: don't pass @Version in installFromLockfile to
  prevent lockfile overwrite before checksum verification
- plugin_install.go: add verifyInstalledPlugin() call in installFromURL
  for parity with registry installs
- engine.go: add TODO to move auto-fetch before plugin discovery so
  newly fetched plugins are available without restart
- integrity_test.go: add tests for unreadable and malformed lockfile
  to verify fail-closed behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 15, 2026 01:40
- registry_source.go: add nolint:gosimple for S1016 — explicit field
  assignment preferred for clarity across different struct tags
- generator_test.go: add TestGenerateProjectStructure verifying all
  generated files exist and use correct external SDK imports/types

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@intel352 intel352 force-pushed the feat/plugin-ecosystem-engine branch from 2bd0d08 to 29f63ff Compare March 15, 2026 01:42
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds opt-in external plugin auto-fetch and lockfile-based integrity verification to improve operational ergonomics and supply-chain safety, and expands wfctl’s plugin tooling (registry sources + scaffold/project layout) plus authoring docs.

Changes:

  • Add engine-side external plugin declarations (plugins.external) with optional auto-fetch via wfctl plugin install.
  • Add SHA-256 verification of installed plugin binaries against .wfctl.yaml on external plugin load.
  • Extend wfctl registry support (static HTTP index), plugin install modes (URL/local/lockfile), and plugin init scaffolding + docs.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
plugin/sdk/generator_test.go Adds tests to validate the generated plugin project structure and SDK usage.
plugin/sdk/generator.go Expands plugin scaffolding to generate a full project layout (cmd/, internal/, CI, GoReleaser, Makefile, README).
plugin/integrity_test.go Adds tests for lockfile discovery and SHA-256 integrity verification behavior.
plugin/integrity.go Implements lockfile lookup + binary SHA-256 verification for external plugins.
plugin/external/manager.go Wires integrity verification into external plugin loading.
plugin/autofetch_test.go Adds unit tests for auto-fetch behaviors and constraint stripping.
plugin/autofetch.go Implements opt-in auto-fetch via shelling out to wfctl plugin install.
engine.go Introduces external plugin dir configuration + triggers auto-fetch during BuildFromConfig.
docs/PLUGIN_AUTHORING.md Adds a comprehensive plugin authoring guide.
config/config_test.go Adds parsing tests for the new plugins.external config section.
config/config.go Introduces PluginsConfig and ExternalPluginDecl config types.
cmd/wfctl/registry_source_test.go Adds tests for the new static-registry source.
cmd/wfctl/registry_source.go Adds StaticRegistrySource (index.json + manifest.json over HTTP).
cmd/wfctl/registry_config.go Updates registry config to support static sources; changes default registry ordering.
cmd/wfctl/plugin_lockfile.go Extends lockfile entries with sha256/registry and adds checksum verification on lockfile install.
cmd/wfctl/plugin_install_new_test.go Adds test coverage for URL/local installs, checksum verification helpers, and copy behavior.
cmd/wfctl/plugin_install.go Adds --url/--local install modes, lockfile SHA updates, and checksum helpers.
cmd/wfctl/plugin_init_test.go Adds tests for wfctl plugin init scaffold output and YAML validity.
cmd/wfctl/plugin.go Adds -module option to plugin init and passes it to the generator.
cmd/wfctl/multi_registry_test.go Updates tests to reflect new default registry config (static + GitHub fallback).
cmd/wfctl/multi_registry.go Adds wiring for static registry sources.
cmd/server/main.go Passes external plugin dir into the engine so BuildFromConfig can trigger auto-fetch.

Comment on lines 81 to 85
// Pass just the name (no @version) so runPluginInstall does not
// call updateLockfile and inadvertently overwrite the pinned entry.
// trigger lockfile updates that would overwrite the pinned entry
// before we verify the checksum.
installArgs = append(installArgs, name)
if err := runPluginInstall(installArgs); err != nil {
engine.go Outdated
Comment on lines +404 to +408
// Auto-fetch declared external plugins before validating requirements.
// TODO: Move auto-fetch before external plugin discovery/loading so newly
// fetched plugins are available in the current process. Currently auto-fetch
// runs after DiscoverPlugins/LoadPlugin in the server startup sequence, so
// plugins downloaded here require a server restart to take effect.
Comment on lines +241 to +243
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: Added registryHTTPClient with 30s timeout, used by StaticRegistrySource.fetch instead of http.DefaultClient.

b.WriteString("}\n\n")
fmt.Fprintf(&b, "func (p *%s) Manifest() sdk.PluginManifest {\n", typeName)
b.WriteString("\treturn sdk.PluginManifest{\n")
fmt.Fprintf(&b, "\t\tName: %q,\n", "workflow-plugin-"+shortName)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: changed space detection from strings.Count(version, " ") > 1 to strings.ContainsRune(version, ' ') — any space now triggers compound constraint fallback.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional: the workflow-plugin- prefix in generated Manifest() is the convention for external plugins. The plugin.json uses opts.Name (short name) which is what the engine matches on disk.

Comment on lines +107 to +109
// Version is an optional version specifier forwarded to wfctl plugin install
// as name@version. Simple constraints (>=, ^, ~) are stripped to extract the
// version; compound constraints fall back to installing the latest.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment accurately describes the behavior: simple constraints are stripped to extract the version, compound constraints fall back to latest. This is the intended semantic.

engine.go Outdated
Comment on lines +411 to +415
var sl *slog.Logger
if l, ok := e.logger.(*slog.Logger); ok {
sl = l
}
decls := make([]plugin.AutoFetchDecl, len(cfg.Plugins.External))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer applicable — auto-fetch was moved from engine.go to cmd/server/main.go where the slog.Logger is directly available.

Comment on lines +81 to +93
// Detect compound constraints such as ">=0.1.0,<0.2.0" or ">=0.1.0 <0.2.0".
if strings.Contains(version, ",") || strings.Count(version, " ") > 1 {
return "", false
}

v := version
for _, p := range []string{">=", "<=", "!=", "^", "~", ">", "<"} {
if strings.HasPrefix(v, p) {
v = v[len(p):]
break
}
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: stripVersionConstraint now treats any whitespace as compound constraint (uses ContainsRune for space detection), catching inputs like "0.1.0 0.2.0". Added test case.


```json
{
"name": "workflow-plugin-my-plugin",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: plugin.json example now uses short name ("my-plugin") matching scaffold output.

Comment on lines +54 to +57
// Create a lockfile with no read permission.
p := filepath.Join(dir, ".wfctl.yaml")
if err := os.WriteFile(p, []byte("plugins:\n my-plugin:\n sha256: abc\n"), 0000); err != nil {
t.Fatalf("write lockfile: %v", err)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This project targets Linux/macOS (Go cross-compile for those two). Windows CI is not in the matrix. The 0000 permission test is adequate for the supported platforms.

Comment on lines +51 to +55
binaryPath := filepath.Join(pluginDir, pluginName, pluginName)
binaryData, err := os.ReadFile(binaryPath)
if err != nil {
return fmt.Errorf("read plugin binary %s: %w", binaryPath, err)
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above — startup-only, single-digit MB binaries. Not worth the added complexity.

intel352 and others added 2 commits March 14, 2026 22:10
Both branches made equivalent fixes to shared wfctl files. Resolved
by taking main's versions for shared code (plugin_install, lockfile,
registry_source, generator, docs) while preserving engine-branch-only
additions (integrity, autofetch, engine auto-fetch, config).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…immediately

Auto-fetch was running inside BuildFromConfig, which executes after
external plugins are already discovered and loaded. Plugins downloaded
by auto-fetch required a server restart to take effect.

Move auto-fetch to buildEngine in cmd/server/main.go, before
DiscoverPlugins/LoadPlugin. Remove the now-unused externalPluginDir
field and SetExternalPluginDir from the engine.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 15, 2026 02:21
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds opt-in external plugin auto-fetching at startup and introduces SHA-256 integrity verification of external plugin binaries against .wfctl.yaml, alongside new config plumbing and expanded scaffold/generator tests.

Changes:

  • Add plugin/autofetch.go (+ tests) to optionally install missing external plugins by shelling out to wfctl.
  • Add plugin/integrity.go (+ tests) to verify installed plugin binaries against lockfile SHA-256 before loading.
  • Extend workflow config with plugins.external[] declarations and wire auto-fetch into the server startup path prior to external plugin discovery.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
plugin/sdk/generator_test.go Adds coverage for full plugin project scaffold output.
plugin/integrity.go Implements lockfile lookup + SHA-256 verification for plugin binaries.
plugin/integrity_test.go Adds unit tests for lockfile search and integrity verification behavior.
plugin/external/manager.go Integrates integrity verification into external plugin load path.
plugin/autofetch.go Implements opt-in auto-fetch via wfctl plugin install.
plugin/autofetch_test.go Adds unit tests for auto-fetch behavior and version constraint parsing.
config/config.go Introduces ExternalPluginDecl and adds WorkflowConfig.Plugins.
config/config_test.go Adds parsing tests for plugins.external declarations.
cmd/server/main.go Calls auto-fetch for declared external plugins before discovery/loading.
cmd/wfctl/plugin_init_test.go Adds/extends scaffold validation tests for wfctl plugin init.
engine.go Minor formatting-only change.

Comment on lines +50 to +66
func isPluginInstalled(pluginName, pluginDir string) bool {
if _, err := os.Stat(filepath.Join(pluginDir, pluginName, "plugin.json")); err == nil {
return true
}

// Also check the alternate naming convention.
const prefix = "workflow-plugin-"
var alt string
if strings.HasPrefix(pluginName, prefix) {
// e.g. "workflow-plugin-foo" → check "foo"
alt = pluginName[len(prefix):]
} else {
// e.g. "foo" → check "workflow-plugin-foo"
alt = prefix + pluginName
}
if _, err := os.Stat(filepath.Join(pluginDir, alt, "plugin.json")); err == nil {
return true
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

plugin.json is the authoritative install marker. If manifest exists but binary is missing, it will fail at plugin load time (ExternalPluginManager) with a clear error. Double-checking here would duplicate the load-time validation.

Comment on lines +51 to +58
binaryPath := filepath.Join(pluginDir, pluginName, pluginName)
binaryData, err := os.ReadFile(binaryPath)
if err != nil {
return fmt.Errorf("read plugin binary %s: %w", binaryPath, err)
}

h := sha256.Sum256(binaryData)
got := hex.EncodeToString(h[:])
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Plugin binaries are typically single-digit MB. os.ReadFile is simpler and this runs once at startup per plugin. Would optimize if profiling shows issues.

Comment on lines +192 to +194
// TestVerifyPluginIntegrity_ChecksumMismatch_CaseInsensitive verifies that the
// comparison is case-insensitive (hex digits may be upper or lower case).
func TestVerifyPluginIntegrity_ChecksumMismatch_CaseInsensitive(t *testing.T) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test name describes the scenario variant (case-insensitive comparison), not the expected outcome. The suffix pattern _CaseInsensitive clarifies what differentiates this test from the mismatch test.

Comment on lines +268 to +272
// Should use GoReleaser.
if !strings.Contains(content, "goreleaser") {
t.Error("release.yml: expected 'goreleaser' action reference")
}
_ = binaryName // variable used for documentation
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The binary name is validated by the release.yml content assertion. The _ = binaryName is documenting the expected value for readability.

Comment on lines +83 to +87
// TestAutoFetchPlugin_CorrectArgs verifies that AutoFetchPlugin constructs the
// expected wfctl arguments. We do this by ensuring the function short-circuits
// when the plugin is already installed (not executing wfctl), which confirms
// the plugin.json check is evaluated before any exec.Command call.
func TestAutoFetchPlugin_CorrectArgs(t *testing.T) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test name is accurate — it verifies the short-circuit path (plugin.json check before exec.Command). The comment on lines 83-85 documents this strategy. Exec stubbing would add complexity without meaningful coverage gain.

Comment on lines 129 to 133
Pipelines map[string]any `json:"pipelines,omitempty" yaml:"pipelines,omitempty"`
Platform map[string]any `json:"platform,omitempty" yaml:"platform,omitempty"`
Requires *RequiresConfig `json:"requires,omitempty" yaml:"requires,omitempty"`
Plugins *PluginsConfig `json:"plugins,omitempty" yaml:"plugins,omitempty"`
Sidecars []SidecarConfig `json:"sidecars,omitempty" yaml:"sidecars,omitempty"`
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: processImports now merges Plugins.External from imported configs with first-definition-wins deduplication, consistent with Modules and Sidecars.

- Merge external plugin declarations from imported configs in processImports
- Fix whitespace-separated version constraint detection (e.g. "0.1.0 0.2.0")
- Add HTTP timeout (30s) to static registry source client
- Align plugin.json doc example name with scaffold output

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@intel352 intel352 merged commit db6c04d into main Mar 15, 2026
15 checks passed
@intel352 intel352 deleted the feat/plugin-ecosystem-engine branch March 15, 2026 03:04
intel352 added a commit that referenced this pull request Mar 15, 2026
- generator.go: use plugin/external/sdk imports and types (PluginProvider,
  StepInstance, StepResult, StepTypes/CreateStep) instead of plugin/sdk
- PLUGIN_AUTHORING.md: update examples to match external SDK interfaces
- plugin_install.go: hash installed binary (not archive) for lockfile,
  add hashFileSHA256 helper, add install mode mutual exclusivity check,
  update installFromLocal to write lockfile, normalize plugin names
- plugin_lockfile.go: add registry param to updateLockfileWithChecksum,
  pass version/registry in installFromLockfile, remove dir on mismatch
- registry_source.go: validate URL in NewStaticRegistrySource
- config.go: clarify Version field forwarding semantics

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
intel352 added a commit that referenced this pull request Mar 15, 2026
- registry_source.go: use explicit field assignment for PluginSummary
  instead of struct type conversion (clearer, avoids tag confusion)
- plugin_lockfile.go: don't pass @Version in installFromLockfile to
  prevent lockfile overwrite before checksum verification
- plugin_install.go: add verifyInstalledPlugin() call in installFromURL
  for parity with registry installs
- engine.go: add TODO to move auto-fetch before plugin discovery so
  newly fetched plugins are available without restart
- integrity_test.go: add tests for unreadable and malformed lockfile
  to verify fail-closed behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
intel352 added a commit that referenced this pull request Mar 15, 2026
* feat(wfctl): add plugin install --url for direct URL installs

Adds --url flag to wfctl plugin install that downloads a tar.gz archive
from a direct URL, extracts plugin.json to identify the plugin name,
installs to the plugin directory, and records the SHA-256 checksum in
the lockfile.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(wfctl): enhanced plugin init scaffold with full project structure

Extends wfctl plugin init to generate cmd/workflow-plugin-<name>/main.go,
internal/provider.go, internal/steps.go, go.mod, .goreleaser.yml, CI/release
GitHub Actions workflows, Makefile, and README.md. Adds --module flag for
custom Go module paths. Preserves existing plugin.json and .go skeleton for
backward compatibility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: engine auto-fetch for declared external plugins on startup

Add AutoFetchPlugin and AutoFetchDeclaredPlugins to plugin/autofetch.go,
which shell out to wfctl to download plugins not found locally. Extend
WorkflowConfig with a new PluginsConfig / ExternalPluginDecl type so
configs can declare plugins with autoFetch: true and an optional version
constraint. StdEngine gains SetExternalPluginDir and calls
AutoFetchDeclaredPlugins in BuildFromConfig before module loading.
The server's buildEngine registers the plugin dir so auto-fetch is active
at runtime. If wfctl is absent, a warning is logged and startup continues.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: resolve all CI lint failures

- Use struct conversion for staticIndexEntry → PluginSummary (staticcheck S1016)
- Remove unused updateLockfile and writePluginJSON functions
- Add nilerr annotations for intentional nil returns in integrity.go
- Add gosec annotation for exec.Command in autofetch.go
- Fix TestLoadRegistryConfigDefault to use DefaultRegistryConfig() directly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address Copilot review feedback on engine PR

- integrity.go: fail closed when lockfile exists but is unreadable or
  unparseable, preventing integrity enforcement bypass
- autofetch.go: extract stripVersionConstraint helper; detect compound
  version constraints and fall back to latest; check both pluginName and
  workflow-plugin-<name> alternate form for installed-check; log restart
  warning when new plugins are downloaded (they require a server restart)
- autofetch_test.go: test stripVersionConstraint directly instead of
  duplicating the logic inline; add compound-constraint cases
- engine.go: clarify comment that auto-fetched plugins need a restart

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address all Copilot review comments on engine PR (#330)

- generator.go: use plugin/external/sdk imports and types (PluginProvider,
  StepInstance, StepResult, StepTypes/CreateStep) instead of plugin/sdk
- PLUGIN_AUTHORING.md: update examples to match external SDK interfaces
- plugin_install.go: hash installed binary (not archive) for lockfile,
  add hashFileSHA256 helper, add install mode mutual exclusivity check,
  update installFromLocal to write lockfile, normalize plugin names
- plugin_lockfile.go: add registry param to updateLockfileWithChecksum,
  pass version/registry in installFromLockfile, remove dir on mismatch
- registry_source.go: validate URL in NewStaticRegistrySource
- config.go: clarify Version field forwarding semantics

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address remaining Copilot review comments on engine PR (#330)

- registry_source.go: use explicit field assignment for PluginSummary
  instead of struct type conversion (clearer, avoids tag confusion)
- plugin_lockfile.go: don't pass @Version in installFromLockfile to
  prevent lockfile overwrite before checksum verification
- plugin_install.go: add verifyInstalledPlugin() call in installFromURL
  for parity with registry installs
- engine.go: add TODO to move auto-fetch before plugin discovery so
  newly fetched plugins are available without restart
- integrity_test.go: add tests for unreadable and malformed lockfile
  to verify fail-closed behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: suppress S1016 lint, add generator project structure tests

- registry_source.go: add nolint:gosimple for S1016 — explicit field
  assignment preferred for clarity across different struct tags
- generator_test.go: add TestGenerateProjectStructure verifying all
  generated files exist and use correct external SDK imports/types

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: move auto-fetch before plugin discovery so fetched plugins load immediately

Auto-fetch was running inside BuildFromConfig, which executes after
external plugins are already discovered and loaded. Plugins downloaded
by auto-fetch required a server restart to take effect.

Move auto-fetch to buildEngine in cmd/server/main.go, before
DiscoverPlugins/LoadPlugin. Remove the now-unused externalPluginDir
field and SetExternalPluginDir from the engine.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add step.workflow_call for cross-pipeline dispatch

Enables pipelines to call other pipelines by name with full context
forwarding. Supports template-resolved workflow names and stop_pipeline
option. Required for WebSocket message routing to game-specific pipelines.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address all 9 Copilot review comments on PR #331

- registry_source.go: switch GitHubRegistrySource to use registryHTTPClient
  (timeout-configured) for both ListPlugins and FetchManifest; update comment
- autofetch.go: scan for AutoFetch=true entries before exec.LookPath to avoid
  misleading startup warning when no plugins need auto-fetch
- autofetch.go: add internal autoFetchPlugin helper that accepts *slog.Logger
  and emits structured log entries when a logger is available; public
  AutoFetchPlugin delegates to it; AutoFetchDeclaredPlugins passes its logger
- autofetch_test.go: rename TestAutoFetchPlugin_CorrectArgs to
  TestAutoFetchPlugin_SkipsWhenExists to match what the test actually asserts
- integrity.go: replace os.ReadFile + sha256.Sum256 with streaming
  os.Open + io.Copy into sha256.New() to keep memory bounded for large binaries
- integrity_test.go: add t.Skip guard in UnreadableLockfile test when the file
  is actually readable (Windows / root environments)
- pipeline_step_workflow_call.go: use resolved workflowName (not s.workflow)
  in async return payload and sync error message for consistency
- docs/PLUGIN_AUTHORING.md: clarify the distinction between plugin.json name
  (short, used by engine) and the registry/provider manifest name
  (workflow-plugin- prefixed), and which is used for dependency resolution
- config/config.go: MergeApplicationConfig now merges Plugins.External from
  each referenced workflow file into the combined config, deduplicated by name

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove duplicate hashFileSHA256 from merge conflict resolution

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address new PR #331 review comments

- docs/PLUGIN_AUTHORING.md: close unclosed code fence after StepProvider example
- cmd/wfctl/plugin_install.go: streaming hashFileSHA256 via io.Copy + sha256.New()
- cmd/wfctl/plugin_install.go: warn on hash failure instead of silent empty checksum
- config/merge_test.go: add TestMergeApplicationConfig_PluginDedup covering first-definition-wins dedup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address 4 new PR #331 review comments

- pipeline_step_workflow_call.go: propagate stop_pipeline in async mode
- integrity.go: "open plugin binary" instead of "read" for os.Open error
- plugin_install.go: lowercase "warning:" for consistency
- merge_test.go: use filepath.Join and check writeFileContent errors

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
intel352 added a commit that referenced this pull request Mar 16, 2026
* feat(wfctl): add plugin install --url for direct URL installs

Adds --url flag to wfctl plugin install that downloads a tar.gz archive
from a direct URL, extracts plugin.json to identify the plugin name,
installs to the plugin directory, and records the SHA-256 checksum in
the lockfile.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(wfctl): enhanced plugin init scaffold with full project structure

Extends wfctl plugin init to generate cmd/workflow-plugin-<name>/main.go,
internal/provider.go, internal/steps.go, go.mod, .goreleaser.yml, CI/release
GitHub Actions workflows, Makefile, and README.md. Adds --module flag for
custom Go module paths. Preserves existing plugin.json and .go skeleton for
backward compatibility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: engine auto-fetch for declared external plugins on startup

Add AutoFetchPlugin and AutoFetchDeclaredPlugins to plugin/autofetch.go,
which shell out to wfctl to download plugins not found locally. Extend
WorkflowConfig with a new PluginsConfig / ExternalPluginDecl type so
configs can declare plugins with autoFetch: true and an optional version
constraint. StdEngine gains SetExternalPluginDir and calls
AutoFetchDeclaredPlugins in BuildFromConfig before module loading.
The server's buildEngine registers the plugin dir so auto-fetch is active
at runtime. If wfctl is absent, a warning is logged and startup continues.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: resolve all CI lint failures

- Use struct conversion for staticIndexEntry → PluginSummary (staticcheck S1016)
- Remove unused updateLockfile and writePluginJSON functions
- Add nilerr annotations for intentional nil returns in integrity.go
- Add gosec annotation for exec.Command in autofetch.go
- Fix TestLoadRegistryConfigDefault to use DefaultRegistryConfig() directly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address Copilot review feedback on engine PR

- integrity.go: fail closed when lockfile exists but is unreadable or
  unparseable, preventing integrity enforcement bypass
- autofetch.go: extract stripVersionConstraint helper; detect compound
  version constraints and fall back to latest; check both pluginName and
  workflow-plugin-<name> alternate form for installed-check; log restart
  warning when new plugins are downloaded (they require a server restart)
- autofetch_test.go: test stripVersionConstraint directly instead of
  duplicating the logic inline; add compound-constraint cases
- engine.go: clarify comment that auto-fetched plugins need a restart

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address all Copilot review comments on engine PR (#330)

- generator.go: use plugin/external/sdk imports and types (PluginProvider,
  StepInstance, StepResult, StepTypes/CreateStep) instead of plugin/sdk
- PLUGIN_AUTHORING.md: update examples to match external SDK interfaces
- plugin_install.go: hash installed binary (not archive) for lockfile,
  add hashFileSHA256 helper, add install mode mutual exclusivity check,
  update installFromLocal to write lockfile, normalize plugin names
- plugin_lockfile.go: add registry param to updateLockfileWithChecksum,
  pass version/registry in installFromLockfile, remove dir on mismatch
- registry_source.go: validate URL in NewStaticRegistrySource
- config.go: clarify Version field forwarding semantics

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address remaining Copilot review comments on engine PR (#330)

- registry_source.go: use explicit field assignment for PluginSummary
  instead of struct type conversion (clearer, avoids tag confusion)
- plugin_lockfile.go: don't pass @Version in installFromLockfile to
  prevent lockfile overwrite before checksum verification
- plugin_install.go: add verifyInstalledPlugin() call in installFromURL
  for parity with registry installs
- engine.go: add TODO to move auto-fetch before plugin discovery so
  newly fetched plugins are available without restart
- integrity_test.go: add tests for unreadable and malformed lockfile
  to verify fail-closed behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: suppress S1016 lint, add generator project structure tests

- registry_source.go: add nolint:gosimple for S1016 — explicit field
  assignment preferred for clarity across different struct tags
- generator_test.go: add TestGenerateProjectStructure verifying all
  generated files exist and use correct external SDK imports/types

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: move auto-fetch before plugin discovery so fetched plugins load immediately

Auto-fetch was running inside BuildFromConfig, which executes after
external plugins are already discovered and loaded. Plugins downloaded
by auto-fetch required a server restart to take effect.

Move auto-fetch to buildEngine in cmd/server/main.go, before
DiscoverPlugins/LoadPlugin. Remove the now-unused externalPluginDir
field and SetExternalPluginDir from the engine.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add step.workflow_call for cross-pipeline dispatch

Enables pipelines to call other pipelines by name with full context
forwarding. Supports template-resolved workflow names and stop_pipeline
option. Required for WebSocket message routing to game-specific pipelines.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address all 9 Copilot review comments on PR #331

- registry_source.go: switch GitHubRegistrySource to use registryHTTPClient
  (timeout-configured) for both ListPlugins and FetchManifest; update comment
- autofetch.go: scan for AutoFetch=true entries before exec.LookPath to avoid
  misleading startup warning when no plugins need auto-fetch
- autofetch.go: add internal autoFetchPlugin helper that accepts *slog.Logger
  and emits structured log entries when a logger is available; public
  AutoFetchPlugin delegates to it; AutoFetchDeclaredPlugins passes its logger
- autofetch_test.go: rename TestAutoFetchPlugin_CorrectArgs to
  TestAutoFetchPlugin_SkipsWhenExists to match what the test actually asserts
- integrity.go: replace os.ReadFile + sha256.Sum256 with streaming
  os.Open + io.Copy into sha256.New() to keep memory bounded for large binaries
- integrity_test.go: add t.Skip guard in UnreadableLockfile test when the file
  is actually readable (Windows / root environments)
- pipeline_step_workflow_call.go: use resolved workflowName (not s.workflow)
  in async return payload and sync error message for consistency
- docs/PLUGIN_AUTHORING.md: clarify the distinction between plugin.json name
  (short, used by engine) and the registry/provider manifest name
  (workflow-plugin- prefixed), and which is used for dependency resolution
- config/config.go: MergeApplicationConfig now merges Plugins.External from
  each referenced workflow file into the combined config, deduplicated by name

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove duplicate hashFileSHA256 from merge conflict resolution

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address new PR #331 review comments

- docs/PLUGIN_AUTHORING.md: close unclosed code fence after StepProvider example
- cmd/wfctl/plugin_install.go: streaming hashFileSHA256 via io.Copy + sha256.New()
- cmd/wfctl/plugin_install.go: warn on hash failure instead of silent empty checksum
- config/merge_test.go: add TestMergeApplicationConfig_PluginDedup covering first-definition-wins dedup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address 4 new PR #331 review comments

- pipeline_step_workflow_call.go: propagate stop_pipeline in async mode
- integrity.go: "open plugin binary" instead of "read" for os.Open error
- plugin_install.go: lowercase "warning:" for consistency
- merge_test.go: use filepath.Join and check writeFileContent errors

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add skip_if and if fields to pipeline step execution

Steps now support optional `skip_if` and `if` Go template guards.
When `skip_if` evaluates to a truthy value the step is skipped and
the pipeline continues; `if` is the logical inverse. Skipped steps
produce `{"skipped": true, "reason": "..."}` output so downstream
steps can inspect the result.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
intel352 added a commit that referenced this pull request Mar 16, 2026
…adata keys for skipped output (#333)

* feat(wfctl): add plugin install --url for direct URL installs

Adds --url flag to wfctl plugin install that downloads a tar.gz archive
from a direct URL, extracts plugin.json to identify the plugin name,
installs to the plugin directory, and records the SHA-256 checksum in
the lockfile.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(wfctl): enhanced plugin init scaffold with full project structure

Extends wfctl plugin init to generate cmd/workflow-plugin-<name>/main.go,
internal/provider.go, internal/steps.go, go.mod, .goreleaser.yml, CI/release
GitHub Actions workflows, Makefile, and README.md. Adds --module flag for
custom Go module paths. Preserves existing plugin.json and .go skeleton for
backward compatibility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: engine auto-fetch for declared external plugins on startup

Add AutoFetchPlugin and AutoFetchDeclaredPlugins to plugin/autofetch.go,
which shell out to wfctl to download plugins not found locally. Extend
WorkflowConfig with a new PluginsConfig / ExternalPluginDecl type so
configs can declare plugins with autoFetch: true and an optional version
constraint. StdEngine gains SetExternalPluginDir and calls
AutoFetchDeclaredPlugins in BuildFromConfig before module loading.
The server's buildEngine registers the plugin dir so auto-fetch is active
at runtime. If wfctl is absent, a warning is logged and startup continues.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: resolve all CI lint failures

- Use struct conversion for staticIndexEntry → PluginSummary (staticcheck S1016)
- Remove unused updateLockfile and writePluginJSON functions
- Add nilerr annotations for intentional nil returns in integrity.go
- Add gosec annotation for exec.Command in autofetch.go
- Fix TestLoadRegistryConfigDefault to use DefaultRegistryConfig() directly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address Copilot review feedback on engine PR

- integrity.go: fail closed when lockfile exists but is unreadable or
  unparseable, preventing integrity enforcement bypass
- autofetch.go: extract stripVersionConstraint helper; detect compound
  version constraints and fall back to latest; check both pluginName and
  workflow-plugin-<name> alternate form for installed-check; log restart
  warning when new plugins are downloaded (they require a server restart)
- autofetch_test.go: test stripVersionConstraint directly instead of
  duplicating the logic inline; add compound-constraint cases
- engine.go: clarify comment that auto-fetched plugins need a restart

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address all Copilot review comments on engine PR (#330)

- generator.go: use plugin/external/sdk imports and types (PluginProvider,
  StepInstance, StepResult, StepTypes/CreateStep) instead of plugin/sdk
- PLUGIN_AUTHORING.md: update examples to match external SDK interfaces
- plugin_install.go: hash installed binary (not archive) for lockfile,
  add hashFileSHA256 helper, add install mode mutual exclusivity check,
  update installFromLocal to write lockfile, normalize plugin names
- plugin_lockfile.go: add registry param to updateLockfileWithChecksum,
  pass version/registry in installFromLockfile, remove dir on mismatch
- registry_source.go: validate URL in NewStaticRegistrySource
- config.go: clarify Version field forwarding semantics

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address remaining Copilot review comments on engine PR (#330)

- registry_source.go: use explicit field assignment for PluginSummary
  instead of struct type conversion (clearer, avoids tag confusion)
- plugin_lockfile.go: don't pass @Version in installFromLockfile to
  prevent lockfile overwrite before checksum verification
- plugin_install.go: add verifyInstalledPlugin() call in installFromURL
  for parity with registry installs
- engine.go: add TODO to move auto-fetch before plugin discovery so
  newly fetched plugins are available without restart
- integrity_test.go: add tests for unreadable and malformed lockfile
  to verify fail-closed behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: suppress S1016 lint, add generator project structure tests

- registry_source.go: add nolint:gosimple for S1016 — explicit field
  assignment preferred for clarity across different struct tags
- generator_test.go: add TestGenerateProjectStructure verifying all
  generated files exist and use correct external SDK imports/types

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: move auto-fetch before plugin discovery so fetched plugins load immediately

Auto-fetch was running inside BuildFromConfig, which executes after
external plugins are already discovered and loaded. Plugins downloaded
by auto-fetch required a server restart to take effect.

Move auto-fetch to buildEngine in cmd/server/main.go, before
DiscoverPlugins/LoadPlugin. Remove the now-unused externalPluginDir
field and SetExternalPluginDir from the engine.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add step.workflow_call for cross-pipeline dispatch

Enables pipelines to call other pipelines by name with full context
forwarding. Supports template-resolved workflow names and stop_pipeline
option. Required for WebSocket message routing to game-specific pipelines.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address all 9 Copilot review comments on PR #331

- registry_source.go: switch GitHubRegistrySource to use registryHTTPClient
  (timeout-configured) for both ListPlugins and FetchManifest; update comment
- autofetch.go: scan for AutoFetch=true entries before exec.LookPath to avoid
  misleading startup warning when no plugins need auto-fetch
- autofetch.go: add internal autoFetchPlugin helper that accepts *slog.Logger
  and emits structured log entries when a logger is available; public
  AutoFetchPlugin delegates to it; AutoFetchDeclaredPlugins passes its logger
- autofetch_test.go: rename TestAutoFetchPlugin_CorrectArgs to
  TestAutoFetchPlugin_SkipsWhenExists to match what the test actually asserts
- integrity.go: replace os.ReadFile + sha256.Sum256 with streaming
  os.Open + io.Copy into sha256.New() to keep memory bounded for large binaries
- integrity_test.go: add t.Skip guard in UnreadableLockfile test when the file
  is actually readable (Windows / root environments)
- pipeline_step_workflow_call.go: use resolved workflowName (not s.workflow)
  in async return payload and sync error message for consistency
- docs/PLUGIN_AUTHORING.md: clarify the distinction between plugin.json name
  (short, used by engine) and the registry/provider manifest name
  (workflow-plugin- prefixed), and which is used for dependency resolution
- config/config.go: MergeApplicationConfig now merges Plugins.External from
  each referenced workflow file into the combined config, deduplicated by name

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove duplicate hashFileSHA256 from merge conflict resolution

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address new PR #331 review comments

- docs/PLUGIN_AUTHORING.md: close unclosed code fence after StepProvider example
- cmd/wfctl/plugin_install.go: streaming hashFileSHA256 via io.Copy + sha256.New()
- cmd/wfctl/plugin_install.go: warn on hash failure instead of silent empty checksum
- config/merge_test.go: add TestMergeApplicationConfig_PluginDedup covering first-definition-wins dedup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address 4 new PR #331 review comments

- pipeline_step_workflow_call.go: propagate stop_pipeline in async mode
- integrity.go: "open plugin binary" instead of "read" for os.Open error
- plugin_install.go: lowercase "warning:" for consistency
- merge_test.go: use filepath.Join and check writeFileContent errors

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add skip_if and if fields to pipeline step execution

Steps now support optional `skip_if` and `if` Go template guards.
When `skip_if` evaluates to a truthy value the step is skipped and
the pipeline continues; `if` is the logical inverse. Skipped steps
produce `{"skipped": true, "reason": "..."}` output so downstream
steps can inspect the result.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Initial plan

* fix: address review feedback on SkippableStep guard template handling

- Return errors (fail closed) instead of swallowing skip_if/if template failures
- Use _skipped/_error keys in skipped step output (align with ErrorStrategySkip convention)
- Fix misleading comment for If field in config/pipeline.go
- Update tests to reflect new key names; add 2 tests for template error behavior

Co-authored-by: intel352 <77607+intel352@users.noreply.github.com>

---------

Co-authored-by: Jon Langevin <codingsloth@pm.me>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: intel352 <77607+intel352@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants