feat: engine auto-fetch + plugin integrity verification#330
Conversation
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>
There was a problem hiding this comment.
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 initscaffolding 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. |
| 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) |
There was a problem hiding this comment.
Fixed: Generator provider.go uses sdk.PluginProvider/StepInstance/Manifest from plugin/external/sdk.
| // 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
| // 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 |
There was a problem hiding this comment.
Acknowledged: TODO comment added documenting auto-fetch timing limitation; architectural change deferred.
There was a problem hiding this comment.
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.
| wfctl plugin init my-plugin -author MyOrg -description "My custom plugin" | ||
|
|
||
| # Build and test | ||
| cd workflow-plugin-my-plugin | ||
| go mod tidy |
There was a problem hiding this comment.
Fixed: Quick start shows cd my-plugin matching OutputDir default.
⏱ Benchmark Results✅ No significant performance regressions detected. benchstat comparison (baseline → PR)
|
- 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>
There was a problem hiding this comment.
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
wfctlregistry 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. |
| PluginSummary: PluginSummary(e), | ||
| Source: s.name, |
There was a problem hiding this comment.
Fixed: Go 1.8+ struct conversion allowed; nolint:gosimple added where explicit fields used for clarity.
| // 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) | ||
| } |
There was a problem hiding this comment.
Fixed: Lockfile stores binary SHA256 via hashFileSHA256.
| } | ||
| if entry.SHA256 != tarChecksum { | ||
| t.Errorf("lockfile checksum: got %q, want %q", entry.SHA256, tarChecksum) | ||
| } |
There was a problem hiding this comment.
Fixed: Test asserts binary SHA256 hash computed from binary content.
| 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 | ||
| } |
There was a problem hiding this comment.
Fixed: integrity.go fails closed with error on unreadable/unparseable lockfile.
| 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") |
There was a problem hiding this comment.
Fixed: Generator main.go uses plugin/external/sdk imports.
| # Build and test | ||
| cd workflow-plugin-my-plugin | ||
| go mod tidy |
There was a problem hiding this comment.
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>
|
All Copilot review comments addressed in commit cd00d91:
|
|
All review comments across both rounds have been addressed: Round 1 (addressed in cd00d91):
Round 2: All comments are duplicates of round 1 items already fixed. Specifically:
|
- 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>
All Copilot review comments addressedPushed generator.go — Generated code now uses PLUGIN_AUTHORING.md — Updated all code examples to match external SDK interfaces. Fixed Quick Start directory name ( plugin_install.go — Lockfile SHA-256 now hashes the installed binary (not the archive) across all install paths (registry, URL, local). Added plugin_lockfile.go — registry_source.go — config.go — All tests pass locally ( |
There was a problem hiding this comment.
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
wfctlplugin ecosystem support: static registry source + multi-registry defaults, URL/local install modes, and richerplugin initscaffolding + 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. |
| func isPluginInstalled(pluginName, pluginDir string) bool { | ||
| if _, err := os.Stat(filepath.Join(pluginDir, pluginName, "plugin.json")); err == nil { | ||
| return true | ||
| } |
| // 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) |
cmd/wfctl/plugin_install.go
Outdated
| binaryChecksum := "" | ||
| binaryPath := filepath.Join(pluginDirVal, pluginName, pluginName) | ||
| if cs, hashErr := hashFileSHA256(binaryPath); hashErr == nil { | ||
| binaryChecksum = cs | ||
| } | ||
| updateLockfileWithChecksum(pluginName, manifest.Version, manifest.Repository, sourceName, binaryChecksum) |
cmd/wfctl/plugin_lockfile.go
Outdated
| installArg := name | ||
| if entry.Version != "" { | ||
| installArg = name + "@" + entry.Version | ||
| } | ||
| installArgs = append(installArgs, installArg) |
| for i := 0; i < 4; i++ { | ||
| p := filepath.Join(dir, ".wfctl.yaml") | ||
| if _, err := os.Stat(p); err == nil { | ||
| return p | ||
| } |
cmd/server/main.go
Outdated
| // 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) | ||
|
|
| lf.Plugins[pluginName] = PluginLockEntry{ | ||
| Version: version, | ||
| Repository: repository, | ||
| SHA256: sha256Hash, | ||
| Registry: registry, | ||
| } |
| 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
| @@ -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>
- 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>
2bd0d08 to
29f63ff
Compare
There was a problem hiding this comment.
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 viawfctl plugin install. - Add SHA-256 verification of installed plugin binaries against
.wfctl.yamlon external plugin load. - Extend
wfctlregistry 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. |
| // 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
| // 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. |
cmd/wfctl/registry_source.go
Outdated
| resp, err := http.DefaultClient.Do(req) | ||
| if err != nil { | ||
| return nil, err |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
Fixed: changed space detection from strings.Count(version, " ") > 1 to strings.ContainsRune(version, ' ') — any space now triggers compound constraint fallback.
There was a problem hiding this comment.
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.
| // 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. |
There was a problem hiding this comment.
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
| var sl *slog.Logger | ||
| if l, ok := e.logger.(*slog.Logger); ok { | ||
| sl = l | ||
| } | ||
| decls := make([]plugin.AutoFetchDecl, len(cfg.Plugins.External)) |
There was a problem hiding this comment.
No longer applicable — auto-fetch was moved from engine.go to cmd/server/main.go where the slog.Logger is directly available.
plugin/autofetch.go
Outdated
| // 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 | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
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.
docs/PLUGIN_AUTHORING.md
Outdated
|
|
||
| ```json | ||
| { | ||
| "name": "workflow-plugin-my-plugin", |
There was a problem hiding this comment.
Fixed: plugin.json example now uses short name ("my-plugin") matching scaffold output.
| // 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) |
There was a problem hiding this comment.
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.
| binaryPath := filepath.Join(pluginDir, pluginName, pluginName) | ||
| binaryData, err := os.ReadFile(binaryPath) | ||
| if err != nil { | ||
| return fmt.Errorf("read plugin binary %s: %w", binaryPath, err) | ||
| } |
There was a problem hiding this comment.
Same as above — startup-only, single-digit MB binaries. Not worth the added complexity.
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>
There was a problem hiding this comment.
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 towfctl. - 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. |
| 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 |
There was a problem hiding this comment.
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.
| 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[:]) |
There was a problem hiding this comment.
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.
| // 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) { |
There was a problem hiding this comment.
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.
| // Should use GoReleaser. | ||
| if !strings.Contains(content, "goreleaser") { | ||
| t.Error("release.yml: expected 'goreleaser' action reference") | ||
| } | ||
| _ = binaryName // variable used for documentation |
There was a problem hiding this comment.
The binary name is validated by the release.yml content assertion. The _ = binaryName is documenting the expected value for readability.
| // 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) { |
There was a problem hiding this comment.
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.
| 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"` |
There was a problem hiding this comment.
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>
- 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>
- 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>
* 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>
* 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>
…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>
Summary
plugin/autofetch.go— engine auto-fetches missing plugins on startup (opt-in per plugin)plugin/integrity.go— SHA-256 verification of plugin binaries against lockfile on loadExternalPluginDeclconfig type for declaring plugins withautoFetch: trueand version constraintsBuildFromConfigbefore plugin loadingExternalPluginManager.LoadPlugindocs/PLUGIN_AUTHORING.md— comprehensive plugin authoring guideEngine Config
```yaml
plugins:
external:
- name: my-plugin
autoFetch: true
version: ">=0.1.0"
```
Test plan
go build ./...compilesgo test ./plugin/... -count=1passesautoFetch: true🤖 Generated with Claude Code