Skip to content

PluginService bridge in iacPluginServiceBridge leaves GetManifest unimplemented but NewExternalPluginAdapter requires it #628

@intel352

Description

@intel352

Summary

PR #623 added an `iacPluginServiceBridge` to `plugin/external/sdk/iacserver.go` so wfctl's `GetContractRegistry` call (via `pb.NewPluginServiceClient`) succeeds against strict-cutover IaC plugins. The bridge implements only `GetContractRegistry` and intentionally leaves the rest of `PluginService` (including `GetManifest`) unimplemented:

```go
// plugin/external/sdk/iacserver.go:123-127
// All other PluginService methods (InvokeService, GetManifest, etc.) remain
// unimplemented (via UnimplementedPluginServiceServer) — strict-cutover IaC
// plugins do not support string-dispatch or module/step/trigger contracts.
type iacPluginServiceBridge struct {
pb.UnimplementedPluginServiceServer
grpcSrv *grpc.Server
}
```

But `plugin/external/adapter.go:87` unconditionally calls `GetManifest` on every plugin load:

```go
func NewExternalPluginAdapter(name string, client *PluginClient) (*ExternalPluginAdapter, error) {
ctx := context.Background()
manifest, err := client.client.GetManifest(ctx, &emptypb.Empty{}) // ← always called
if err != nil {
return nil, fmt.Errorf("get manifest from plugin %s: %w", name, err)
}

}
```

Result: every strict-cutover IaC plugin (DO v1.0.1, future eventbus, etc.) hits:

```
error: rpc error: code = Unimplemented desc = method GetManifest not implemented
```

Reproduction

BMW deploy run 25642489809 on sha 196afb4f (PR #271 = DO plugin v1.0.0 → v1.0.1, the rebuild against workflow v0.51.0 SDK with PR #623). wfctl v0.51.0 + DO plugin v1.0.1 — the version pair PR #623 was supposed to unblock:

```
[external-plugins] starting plugin "digitalocean" (version 1.0.1)
[external-plugins] all external plugins shut down
error: rpc error: code = Unimplemented desc = method GetManifest not implemented
```

GetContractRegistry now works (no more "unknown service PluginService"), but the next RPC (GetManifest) trips on the bridge's intentional gap.

Fix options

A. Add a minimal GetManifest stub to iacPluginServiceBridge

The bridge can build a stripped Manifest (name, version, empty moduleTypes/stepTypes/triggerTypes, capabilities=iac-only) by inspecting the registered IaC services + plugin.contracts.json. This keeps the symmetric "plugin reports its surface" model — IaC plugins just report an IaC-only surface.

B. Make wfctl's NewExternalPluginAdapter tolerate Unimplemented GetManifest for IaC plugins

Either:

  • detect strict-cutover-IaC plugins before adapter construction and skip GetManifest, or
  • treat `codes.Unimplemented` from GetManifest as "empty manifest" and proceed (similar to the GetTriggerTypes pattern at line 93).

C. Build the manifest from the contract registry

The `GetContractRegistry` bridge already advertises which services are registered. wfctl can synthesize a Manifest from that without calling GetManifest at all — eliminate the call on the wfctl side.

(A) is the smallest blast radius and keeps the existing call pattern. (C) is most aligned with the strict-cutover spirit but requires wfctl-side changes.

Blast radius

Every consumer pinning wfctl v0.51.0+ AND any strict-cutover IaC plugin (currently just DO v1.0.0+ but future strict-cutover plugins will all hit this). BMW deploy chain stuck here right now (sha 196afb4f).

Related

🤖 Filed by Claude Code on behalf of BMW deploy debug chain

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions