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
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:
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