Skip to content

fix(plugin/external): bridge IaCProvider.EnumerateAll + strict-contracts hardening (v0.27.1)#589

Merged
intel352 merged 5 commits into
mainfrom
feat/enumerator-bridge-strict
May 9, 2026
Merged

fix(plugin/external): bridge IaCProvider.EnumerateAll + strict-contracts hardening (v0.27.1)#589
intel352 merged 5 commits into
mainfrom
feat/enumerator-bridge-strict

Conversation

@intel352

@intel352 intel352 commented May 9, 2026

Copy link
Copy Markdown
Contributor

Bug

wfctl infra audit-keys --type infra.spaces_key failed at staging with:

audit-keys: no loaded provider implements EnumeratorAll

even though workflow-plugin-digitalocean v0.14.0 implements EnumerateAll
correctly on its DOProvider (spaces-key plan Tasks 14+15).

Root cause: v0.27.0 declared the new optional interface interfaces.EnumeratorAll
and added it to plugin-side providers, but the wfctl-side gRPC proxy
*remoteIaCProvider in cmd/wfctl/deploy_providers.go was NOT updated to
implement the new optional interface. The dispatcher in runInfraAuditKeysCmd
type-asserts the loaded provider against interfaces.EnumeratorAll
(cmd/wfctl/infra_audit_keys.go:140), so the assertion always fell through to
the negative branch even though the plugin process implemented the method.

The same root gap exists for interfaces.Enumerator (EnumerateByTag); both
are fixed here.

Why strict-contracts didn't catch it

The pre-existing strict-contracts gate (cmd/wfctl/plugin_audit.go) audits
plugin-side manifest contract descriptors (plugin.contracts.json). It
has no visibility into workflow-side proxy method coverage. Compile-time
interface-satisfaction assertions on *remoteIaCProvider existed only for
IaCProvider + ProviderCredentialRevoker; optional sub-interfaces
(EnumeratorAll, Enumerator, DriftConfigDetector,
ProviderMigrationRepairer) had no enforced coverage.

There was no fallback or skip to remove — the test category simply didn't exist.
The user mandate's "remove the fallback" intent is fulfilled by adding a new
unconditional gate that forecloses this entire class of cross-repo bridge gap.

Fixes

  1. cmd/wfctl/deploy_providers.go — added EnumerateAll(ctx, resourceType)
    and EnumerateByTag(ctx, tag) methods to remoteIaCProvider, dispatching
    via InvokeServiceContext when available (falls back to InvokeService
    for legacy invokers). Modeled exactly on the existing
    RepairDirtyMigration / RevokeProviderCredential pattern.

  2. cmd/wfctl/deploy_providers_strict_bridge_coverage_test.go (new) — three
    strict-contracts coverage gates that catch the entire class of bridge gap:

    • TestStrictBridgeCoverage_CompileTimeAssertions: package-init var _ interfaces.X = (*remoteIaCProvider)(nil) block for every optional sub-interface — adding a new interface without bridging will fail to compile.
    • TestStrictBridgeCoverage_WireMethodCoverage: source-grep asserts every interface method has a corresponding "IaCProvider.<Method>" string literal in deploy_providers.go. Catches typos in RPC method names and "method declared but not actually wired through gRPC" placeholders. Exempt list (Name, Version, Close, etc.) is small + each entry justified.
    • TestStrictBridgeCoverage_NoFallbackOrSkip: meta-check that the gate bodies contain no t.Skip, testing.Short, os.Getenv, or build-tag bypasses. Per the user mandate ("remove the fallback and force strict mode"), the gate is unconditional.
  3. cmd/wfctl/deploy_providers_dispatch_matrix_test.go — added EnumerateAll
    and EnumerateByTag rows to TestDispatchMatrix_RemoteIaCProvider (pins
    wire method name + required arg keys, mirroring the existing
    RepairDirtyMigration row).

  4. cmd/wfctl/deploy_providers_remote_iac_test.go — 8 new tests:

    • TestRemoteIaCProvider_EnumerateAll (happy path)
    • TestRemoteIaCProvider_EnumerateAll_NilResponse (empty account)
    • TestRemoteIaCProvider_EnumerateAll_PropagatesError
    • TestRemoteIaCProvider_EnumerateAll_DecodeError
    • TestRemoteIaCProvider_EnumerateAll_UsesContext
    • TestRemoteIaCProvider_EnumerateByTag (happy path)
    • TestRemoteIaCProvider_EnumerateByTag_PropagatesError
    • TestRemoteIaCProvider_EnumerateByTag_UsesContext

What about server-side dispatch?

The brief originally suggested a Fix 2 in plugin/external/sdk/grpc_server.go
adding a case "IaCProvider.EnumerateAll" next to a RepairDirtyMigration
case. Verified during investigation: the workflow-side grpc_server.go is
method-name-agnostic — it delegates to the plugin's own
InvokeMethod/InvokeMethodContext dispatcher. There are no per-method cases
to extend on the workflow side. The plugin (DO v0.14.0) owns its own method
string → typed-method dispatch, and that side is already correct.

Verification

GOWORK=off go build ./...        # passes
GOWORK=off go test ./...         # all packages pass
GOWORK=off go vet ./cmd/wfctl/... # clean
gofmt -l                         # clean

The pre-existing gosec G115 warning in cmd/wfctl/secrets_detect.go:186
is on main and unrelated to this PR.

References

  • Workspace memory: feedback_workflow_plugin_structpb_boundary (interim until typed proto contracts land)
  • Workspace memory: project_workflow_strict_grpc_contracts (long-term direction)
  • Workspace memory: feedback_no_invented_interfaces, feedback_local_ci_validation_for_ci_touching_tasks, feedback_proper_fixes_over_workarounds (review discipline applied)

🤖 Generated with Claude Code

…RPC + strict-contracts coverage gate (v0.27.1)

The gap
-------
v0.27.0 declared interfaces.EnumeratorAll (used by `wfctl infra audit-keys`
+ `wfctl infra prune`) and added it to plugin-side providers (e.g. DO
v0.14.0 DOProvider.EnumerateAll). The wfctl-side gRPC proxy
*remoteIaCProvider* in cmd/wfctl/deploy_providers.go was NOT updated to
implement the optional interface, so the type-assert in
runInfraAuditKeysCmd (cmd/wfctl/infra_audit_keys.go:140) always fell
through to "no loaded provider implements EnumeratorAll" — even when the
plugin process correctly implemented EnumerateAll.

Same root gap existed for interfaces.Enumerator (EnumerateByTag).

Why strict-contracts didn't catch it
-------------------------------------
The pre-existing strict-contracts gate (cmd/wfctl/plugin_audit.go) audits
PLUGIN-SIDE manifest contract descriptors. It has no visibility into
workflow-side proxy method coverage. Compile-time interface satisfaction
was only asserted for IaCProvider + ProviderCredentialRevoker; optional
sub-interfaces (EnumeratorAll, Enumerator, DriftConfigDetector,
ProviderMigrationRepairer) had no enforced coverage. There was no
fallback or skip to remove — the test category simply didn't exist.

Fixes
-----
1. cmd/wfctl/deploy_providers.go — add EnumerateAll + EnumerateByTag
   methods to remoteIaCProvider, dispatching via
   InvokeServiceContext (with InvokeService fallback for legacy
   invokers), modeled exactly on the existing RepairDirtyMigration
   pattern.

2. cmd/wfctl/deploy_providers_strict_bridge_coverage_test.go (new) —
   strict-contracts coverage gate that:
   - compile-time-asserts *remoteIaCProvider satisfies every optional
     IaCProvider sub-interface declared in interfaces/iac_provider.go
     (catches missing-bridge gaps when a new interface is added);
   - source-grep-asserts every interface method has a corresponding
     "IaCProvider.<Method>" string literal in deploy_providers.go
     (catches typos and "wired-but-not-actually-dispatched" bugs);
   - meta-asserts the gate bodies contain no t.Skip, t.SkipNow,
     testing.Short, os.Getenv, or build-tag bypasses (per the v0.27.1
     user mandate "remove the fallback and force strict mode").

3. cmd/wfctl/deploy_providers_dispatch_matrix_test.go — add EnumerateAll
   and EnumerateByTag rows to the IaCProvider dispatch matrix (pins
   wire method name + required arg keys).

4. cmd/wfctl/deploy_providers_remote_iac_test.go — 8 new tests covering
   the new methods: happy path, nil response, error propagation, decode
   error (EnumerateAll only), and context-aware invoker dispatch for
   both methods.

Per workspace memory feedback_workflow_plugin_structpb_boundary +
project_workflow_strict_grpc_contracts: this gate is an INTERIM
defense-in-depth measure until typed proto messages replace the
map[string]any dispatch surface (after which the bridge gap class
becomes a compile error rather than a runtime gap).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 9, 2026 20:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes a wfctl ↔ external-plugin gRPC bridge gap where optional IaC provider sub-interfaces (EnumeratorAll, Enumerator) were implemented by the plugin process but not by the wfctl-side proxy (remoteIaCProvider), causing type-assert based dispatchers (e.g. infra audit-keys) to incorrectly conclude “not implemented”.

Changes:

  • Added EnumerateAll and EnumerateByTag gRPC dispatch methods to *remoteIaCProvider.
  • Added a new “strict bridge coverage” test gate (compile-time interface assertions + source-grep wire coverage + meta no-skip check).
  • Expanded dispatch-matrix + remote-provider unit tests to cover the new enumeration methods.

Reviewed changes

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

File Description
cmd/wfctl/deploy_providers.go Implements EnumerateAll/EnumerateByTag on the wfctl proxy and wires them to InvokeService{,Context}.
cmd/wfctl/deploy_providers_strict_bridge_coverage_test.go Adds strict-contracts style gates to prevent future proxy/bridge method omissions.
cmd/wfctl/deploy_providers_remote_iac_test.go Adds unit tests for EnumerateAll/EnumerateByTag behavior, decoding, error propagation, and context usage.
cmd/wfctl/deploy_providers_dispatch_matrix_test.go Pins RPC method names + required arg keys for the new proxy methods.

Comment thread cmd/wfctl/deploy_providers.go
Comment thread cmd/wfctl/deploy_providers.go
Comment thread cmd/wfctl/deploy_providers_strict_bridge_coverage_test.go Outdated
Comment thread cmd/wfctl/deploy_providers_strict_bridge_coverage_test.go
@github-actions

github-actions Bot commented May 9, 2026

Copy link
Copy Markdown

⏱ Benchmark Results

No significant performance regressions detected.

benchstat comparison (baseline → PR)
## benchstat: baseline → PR
baseline-bench.txt:262: parsing iteration count: invalid syntax
baseline-bench.txt:325460: parsing iteration count: invalid syntax
baseline-bench.txt:661920: parsing iteration count: invalid syntax
baseline-bench.txt:949924: parsing iteration count: invalid syntax
baseline-bench.txt:1311284: parsing iteration count: invalid syntax
baseline-bench.txt:1579623: parsing iteration count: invalid syntax
benchmark-results.txt:262: parsing iteration count: invalid syntax
benchmark-results.txt:348878: parsing iteration count: invalid syntax
benchmark-results.txt:673506: parsing iteration count: invalid syntax
benchmark-results.txt:962641: parsing iteration count: invalid syntax
benchmark-results.txt:1276502: parsing iteration count: invalid syntax
benchmark-results.txt:1583070: parsing iteration count: invalid syntax
goos: linux
goarch: amd64
pkg: github.com/GoCodeAlone/workflow/dynamic
cpu: AMD EPYC 7763 64-Core Processor                
                            │ baseline-bench.txt │       benchmark-results.txt        │
                            │       sec/op       │    sec/op     vs base              │
InterpreterCreation-4              3.540m ± 188%   6.620m ± 66%       ~ (p=0.485 n=6)
ComponentLoad-4                    3.561m ±   2%   3.625m ±  2%  +1.80% (p=0.009 n=6)
ComponentExecute-4                 1.957µ ±   1%   1.973µ ±  1%  +0.87% (p=0.006 n=6)
PoolContention/workers-1-4         1.101µ ±   2%   1.095µ ±  1%       ~ (p=0.368 n=6)
PoolContention/workers-2-4         1.089µ ±   4%   1.088µ ±  3%       ~ (p=0.853 n=6)
PoolContention/workers-4-4         1.100µ ±   1%   1.093µ ±  1%  -0.59% (p=0.028 n=6)
PoolContention/workers-8-4         1.118µ ±   1%   1.098µ ±  0%  -1.74% (p=0.002 n=6)
PoolContention/workers-16-4        1.111µ ±   2%   1.102µ ±  6%       ~ (p=0.335 n=6)
ComponentLifecycle-4               3.689m ±   1%   3.626m ±  1%  -1.71% (p=0.002 n=6)
SourceValidation-4                 2.337µ ±   1%   2.346µ ±  2%       ~ (p=0.119 n=6)
RegistryConcurrent-4               795.1n ±   2%   801.4n ±  2%       ~ (p=0.240 n=6)
LoaderLoadFromString-4             3.771m ±   3%   3.642m ±  1%  -3.44% (p=0.002 n=6)
geomean                            17.84µ          18.72µ        +4.90%

                            │ baseline-bench.txt │        benchmark-results.txt         │
                            │        B/op        │     B/op      vs base                │
InterpreterCreation-4               2.027Mi ± 0%   2.027Mi ± 0%       ~ (p=0.734 n=6)
ComponentLoad-4                     2.180Mi ± 0%   2.180Mi ± 0%       ~ (p=0.788 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.183Mi ± 0%   2.183Mi ± 0%       ~ (p=0.513 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.182Mi ± 0%   2.182Mi ± 0%       ~ (p=0.394 n=6)
geomean                             15.25Ki        15.25Ki       +0.00%
¹ all samples are equal

                            │ baseline-bench.txt │        benchmark-results.txt        │
                            │     allocs/op      │  allocs/op   vs base                │
InterpreterCreation-4                15.68k ± 0%   15.68k ± 0%       ~ (p=1.000 n=6)
ComponentLoad-4                      18.02k ± 0%   18.02k ± 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                 18.07k ± 0%   18.07k ± 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               18.06k ± 0%   18.06k ± 0%       ~ (p=1.000 n=6) ¹
geomean                               183.3         183.3       +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                 285.7n ± 16%   287.0n ± 0%       ~ (p=0.420 n=6)
CircuitBreakerExecution_Success-4         21.52n ±  0%   21.57n ± 1%  +0.26% (p=0.024 n=6)
CircuitBreakerExecution_Failure-4         66.85n ±  0%   66.13n ± 0%  -1.08% (p=0.002 n=6)
geomean                                   74.35n         74.26n       -0.13%

                                  │ 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                     864.6n ± 32%   1003.4n ± 14%       ~ (p=0.180 n=6)
JQTransform_ObjectConstruction-4         1.441µ ±  1%    1.477µ ±  1%  +2.46% (p=0.002 n=6)
JQTransform_ArraySelect-4                3.385µ ±  3%    3.372µ ±  2%       ~ (p=0.937 n=6)
JQTransform_Complex-4                    38.64µ ±  3%    38.44µ ±  1%       ~ (p=0.818 n=6)
JQTransform_Throughput-4                 1.750µ ±  1%    1.785µ ±  1%  +1.97% (p=0.004 n=6)
SSEPublishDelivery-4                     63.09n ±  0%    63.13n ±  1%       ~ (p=0.556 n=6)
geomean                                  1.619µ          1.669µ        +3.12%

                                 │ 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.121µ ± 16%   1.113µ ±  2%       ~ (p=0.485 n=6)
SchemaValidation_AllFields-4                1.667µ ±  3%   1.680µ ± 18%       ~ (p=0.937 n=6)
SchemaValidation_FormatValidation-4         1.604µ ±  1%   1.620µ ±  2%       ~ (p=0.310 n=6)
SchemaValidation_ManySchemas-4              1.810µ ±  4%   1.822µ ±  4%       ~ (p=1.000 n=6)
geomean                                     1.526µ         1.533µ        +0.46%

                                    │ 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.258µ ± 16%   1.192µ ± 13%        ~ (p=0.132 n=6)
EventStoreAppend_SQLite-4                  1.480m ± 14%   1.339m ±  7%   -9.52% (p=0.002 n=6)
GetTimeline_InMemory/events-10-4           14.41µ ±  2%   13.82µ ±  4%   -4.14% (p=0.002 n=6)
GetTimeline_InMemory/events-50-4           81.05µ ±  2%   76.88µ ± 20%   -5.14% (p=0.004 n=6)
GetTimeline_InMemory/events-100-4          165.1µ ±  5%   123.8µ ±  1%  -25.01% (p=0.002 n=6)
GetTimeline_InMemory/events-500-4          765.0µ ± 14%   637.9µ ±  1%  -16.61% (p=0.002 n=6)
GetTimeline_InMemory/events-1000-4         1.370m ±  3%   1.307m ±  2%   -4.64% (p=0.002 n=6)
GetTimeline_SQLite/events-10-4             112.3µ ±  1%   106.3µ ±  1%   -5.32% (p=0.002 n=6)
GetTimeline_SQLite/events-50-4             261.4µ ±  1%   247.8µ ±  1%   -5.20% (p=0.002 n=6)
GetTimeline_SQLite/events-100-4            441.7µ ±  1%   421.4µ ±  2%   -4.59% (p=0.002 n=6)
GetTimeline_SQLite/events-500-4            1.895m ±  2%   1.801m ±  0%   -4.95% (p=0.002 n=6)
GetTimeline_SQLite/events-1000-4           3.619m ±  2%   3.499m ±  1%   -3.30% (p=0.002 n=6)
geomean                                    238.2µ         219.1µ         -8.04%

                                   │ baseline-bench.txt │         benchmark-results.txt         │
                                   │        B/op        │     B/op       vs base                │
EventStoreAppend_InMemory-4                 773.5 ± 12%     776.0 ± 10%       ~ (p=0.669 n=6)
EventStoreAppend_SQLite-4                 1.984Ki ±  2%   1.984Ki ±  2%       ~ (p=0.781 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.141 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%  -0.00% (p=0.015 n=6)
GetTimeline_SQLite/events-1000-4          1.639Mi ±  0%   1.639Mi ±  0%  +0.00% (p=0.013 n=6)
geomean                                   67.22Ki         67.24Ki        +0.03%
¹ 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.

…Validator + harden no-fallback meta-check

Addresses Copilot inline review (PR #589 round 1):

1. Behavior preservation for non-implementing plugins (Findings 1+2)
   ─────────────────────────────────────────────────────────────────
   The round-1 bridge of EnumerateAll + EnumerateByTag made every
   gRPC-loaded provider satisfy the optional interfaces at the type
   level. Dispatchers (infra_audit_keys, infra_cleanup, infra_prune,
   infra_rotate_and_prune) used `p.(interfaces.X)` as the "supports
   this method?" gate; the fall-through-to-next-provider semantics
   regressed once every provider satisfied the assertion.

   Fix: introduce interfaces.ErrProviderMethodUnimplemented sentinel.
   Proxy methods translate gRPC codes.Unimplemented (canonical signal)
   plus a small set of message-string fallbacks ("unimplemented",
   "not implemented", "does not implement ServiceInvoker") into the
   sentinel. Dispatchers errors.Is on the sentinel to preserve the
   pre-v0.27.1 iterate-and-skip behavior.

   - infra_cleanup.go: errors.Is check inside the existing
     enumErr path; emits the same "skipped <provider>: ..." stdout
     line as the type-assert-fail branch.
   - infra_audit_keys.go: probedEnumerator wrapper traps the
     sentinel during runInfraAuditKeys's single call (no double
     RPC) and signals "skip and try next" via a recorded flag.
   - infra_prune.go: probedPruneProvider wrapper applies the same
     pattern; reused by rotate-and-prune dispatcher.
   - infra_rotate_and_prune.go: uses probedPruneProvider.

2. Bridge ProviderValidator (Finding 4)
   ──────────────────────────────────────
   Copilot flagged that interfaces.ProviderValidator was missing from
   the strict-bridge-coverage registry even though
   cmd/wfctl/infra_align_rules.go does `p.(interfaces.ProviderValidator)`
   for R-A10. Bridged by adding ValidatePlan to remoteIaCProvider.

   Contract caveat: ProviderValidator.ValidatePlan returns only
   []PlanDiagnostic (no error). The proxy silences ALL errors
   (Unimplemented + transport + plugin) to preserve the pre-v0.27.1
   R-A10 behavior where non-implementing plugins were silently
   skipped via the type-assert gate. Surfacing transport errors
   through R-A10 requires extending the ProviderValidator contract
   to return an error — left to a follow-up PR per
   feedback_proper_fixes_over_workarounds.

3. Harden no-fallback meta-check (Finding 3)
   ──────────────────────────────────────────
   Copilot caught that the original "build tag" string check was
   ineffective: actual Go build constraints live in the file header
   as //go:build or // +build directives, not in function bodies.

   Fix: two-layer enforcement. Layer 1 scans the file header (above
   the package clause) for either directive form. Layer 2 keeps the
   function-body scan for runtime bypasses (t.Skip, testing.Short,
   os.Getenv). Layer 1 catches bypasses Layer 2 cannot see.

4. Tests
   ─────
   - TestRemoteIaCProvider_EnumerateAll_TranslatesUnimplemented
     (4 sub-cases: gRPC code, "unimplemented" string,
     "not implemented" string, "does not implement ServiceInvoker"
     string).
   - TestRemoteIaCProvider_EnumerateByTag_TranslatesUnimplemented.
   - TestRemoteIaCProvider_ValidatePlan_HappyPath +
     TestRemoteIaCProvider_ValidatePlan_SilentOnError.
   - Strict-bridge-coverage gate: ProviderValidator added to
     compile-time + reflective registries; existing tests still pass.

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.

Comment thread cmd/wfctl/infra_rotate_and_prune.go Outdated
Comment thread cmd/wfctl/infra_audit_keys.go Outdated
Comment thread cmd/wfctl/infra_prune.go Outdated
Comment thread cmd/wfctl/deploy_providers.go Outdated
Comment thread cmd/wfctl/deploy_providers.go
intel352 and others added 2 commits May 9, 2026 17:15
…l loud on missing bridge

- audit-keys: drop probedEnumerator (nil, nil) swallow; return error
- prune: drop probedPruneProvider (nil, nil) swallow; return error
- rotate-and-prune: pre-flight EnumerateAll check BEFORE Step 1 rotation
- deploy_providers: align isPluginMethodUnimplemented + ValidatePlan docs with impl
- tests: 3 new strict-mode regressions assert loud failure on bridge gap

Per user mandate: no fallback. Missing bridge wiring must fail loud, not
silently return zero results.
…ider dispatcher loops

By bridging EnumerateAll/EnumerateByTag through remoteIaCProvider, every gRPC-loaded
provider satisfies the optional interface even if the plugin doesn't actually support
the method. Update dispatcher loops in infra_audit_keys, infra_prune, infra_rotate_and_prune
to call the method, treat ErrProviderMethodUnimplemented as "this provider doesn't
support it, try next", and error loud only if ALL providers fail to implement.

infra_cleanup already had the correct pattern (try-each + skip-on-Unimplemented log
line). Add a regression test asserting it stays correct under heterogeneous loaded
providers.

infra_audit_keys: extract renderAuditKeys() so the dispatcher can call EnumerateAll
directly and inspect the error to continue-on-Unimplemented.

infra_prune: probe EnumerateAll once in the dispatcher; on success cache the result
in cachedPruneProvider so runInfraPrune's internal call serves from cache and we
don't double-bill the cloud API.

infra_rotate_and_prune: the pre-flight probe now continues past Unimplemented
providers rather than aborting the whole run; loud error only fires when no
provider can serve the resource type. Rotation (Step 1, destructive) still
gated behind a successful pre-flight on the chosen provider.

Plus 4 new tests asserting multi-provider continue-on-Unimplemented for
audit-keys, prune, rotate-and-prune, and cleanup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 9, 2026 21:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

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

Comment thread cmd/wfctl/infra_audit_keys.go Outdated
Comment thread cmd/wfctl/deploy_providers.go Outdated
Comment thread cmd/wfctl/infra_rotate_and_prune.go
…patcher comments with impl

- audit-keys: dispatcher comment now describes the direct EnumerateAll +
  renderAuditKeys path. The previous wording referenced synthesizing an
  inner-args slice and dispatching to runInfraAuditKeys, but the impl
  invokes EnumerateAll itself across all providers and renders via
  renderAuditKeys to keep the cloud-API call count to one per provider.
- isPluginMethodUnimplemented: doc lists the literal strings actually
  emitted by plugin/external/sdk/grpc_server.go (verified at line 527 for
  ServiceInvoker; ServiceContextInvoker is NOT emitted today and is kept
  only as a defensive forward-compat catch; TypedServiceInvoker at line
  498 is intentionally not matched because IaCProvider RPCs use the
  untyped path).
- rotate-and-prune: thread the pre-flight EnumerateAll result into
  runInfraRotateAndPrune via cachedPruneProvider so the inner runInfraPrune
  serves from cache rather than re-issuing the cloud enumeration. This
  closes the double-EnumerateAll on the successful path that Copilot
  flagged: previously the dispatcher probed EnumerateAll for verification
  then handed runInfraRotateAndPrune a raw pruneProviderAdapter, and
  runInfraPrune (called after rotation) re-enumerated. The cache key is
  resourceType; the freshly-rotated key is excluded by --exclude-access-key
  in the prune step so serving the pre-rotation snapshot is safe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@intel352 intel352 merged commit 03873d2 into main May 9, 2026
23 checks passed
@intel352 intel352 deleted the feat/enumerator-bridge-strict branch May 9, 2026 22:34
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