feat(proto): add iac.proto with IaCProviderRequired + 6 optional services + ResourceDriver (Task 3)#598
Merged
Merged
Conversation
…ices + ResourceDriver Task 3 of the strict-contracts force-cutover plan (docs/plans/2026-05-10-strict-contracts-force-cutover.md, rev5). Adds plugin/external/proto/iac.proto defining the typed gRPC contract that supersedes the legacy InvokeService/structpb dispatch path for IaCProvider + ResourceDriver: - service IaCProviderRequired: 11 RPCs every IaC plugin MUST implement (Initialize, Name, Version, Capabilities, Plan, Apply, Destroy, Status, Import, ResolveSizing, BootstrapStateBackend). Compile-time enforced via the SDK type-assert in Task 4. - 6 optional services — providers register only the ones they support: IaCProviderEnumerator (EnumerateAll, EnumerateByTag), IaCProviderDriftDetector (DetectDrift, DetectDriftWithSpecs), IaCProviderCredentialRevoker (RevokeProviderCredential), IaCProviderMigrationRepairer (RepairDirtyMigration), IaCProviderValidator (ValidatePlan), IaCProviderDriftConfigDetector (DetectDriftConfig). Absence of registration IS the negative signal — no NotSupported field on any optional response (per design §Optional services). - service ResourceDriver: 9 RPCs for per-resource-type CRUD dispatch (Create, Read, Update, Delete, Diff, Scale, HealthCheck, SensitiveKeys, Troubleshoot), each carrying resource_type so a single server can route to the per-type driver implementation. Hard invariants honored: - NO google.protobuf.Struct, NO google.protobuf.Any anywhere. - Free-form per-resource Config/Outputs payloads cross the wire as bytes <name>_json (the plugin owns json.Marshal/Unmarshal); this eliminates the structpb conversion surface that previously dropped map[string]bool entries silently (T3.9 finding). - ResourceOutput.sensitive uses typed map<string, bool> per design. Generated iac.pb.go + iac_grpc.pb.go via protoc v34.1 + protoc-gen-go v1.36.11 + protoc-gen-go-grpc v1.6.1. Failing test (plugin/external/proto/iac_proto_test.go) asserts the generated server interfaces exist and have the methods the design requires — drops in iac.proto cause the test file to fail to compile. Verification: GOWORK=off go test ./plugin/external/proto/... PASSES; GOWORK=off go build ./plugin/... ./cmd/... ./module/... clean. Rollback: revert this commit; legacy InvokeService dispatch in plugin.proto remains functional; the additive-only nature of this PR means no consumer is affected until subsequent tasks wire callers.
Contributor
There was a problem hiding this comment.
Pull request overview
Adds a new typed gRPC contract (iac.proto) for external IaC plugins (provider + resource driver), along with generated Go bindings and a small “contract shape” test intended to prevent accidental RPC/interface drift during the strict-contracts cutover.
Changes:
- Introduces
plugin/external/proto/iac.protodefiningIaCProviderRequired, six optional provider services, andResourceDriver, using JSON-encodedbytesfields instead ofstructpb. - Adds protoc-generated Go bindings for the new contract (
iac.pb.go,iac_grpc.pb.go). - Adds a conformance-style test file (
iac_proto_test.go) to guard the generated interfaces (but see review comments about the current assertions not actually catching RPC drops).
Reviewed changes
Copilot reviewed 3 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| plugin/external/proto/iac.proto | New typed IaC + ResourceDriver proto contract (no Struct/Any; JSON-bytes payload strategy). |
| plugin/external/proto/iac.pb.go | Generated protobuf message/types for the IaC contract. |
| plugin/external/proto/iac_grpc.pb.go | Generated gRPC client/server bindings for the IaC contract services. |
| plugin/external/proto/iac_proto_test.go | Compile-time “shape” checks for generated server interfaces. |
Comment on lines
+17
to
+25
| func TestIaCProviderRequiredServerHasAllRequiredMethods(t *testing.T) { | ||
| var srv pb.IaCProviderRequiredServer = (*requiredStub)(nil) | ||
| _ = srv // type-assert satisfaction is checked at compile time | ||
|
|
||
| // Methods are checked at compile time via the type assertion above. | ||
| // The stub MUST satisfy: Initialize, Name, Version, Capabilities, | ||
| // Plan, Apply, Destroy, Status, Import, ResolveSizing, | ||
| // BootstrapStateBackend. | ||
| } |
Comment on lines
+54
to
+60
| // TestResourceDriverServerInterfaceExists asserts the generated | ||
| // ResourceDriverServer interface exists with the 9 RPC methods from the | ||
| // design (Create, Read, Update, Delete, Diff, Scale, HealthCheck, | ||
| // SensitiveKeys, Troubleshoot). | ||
| func TestResourceDriverServerInterfaceExists(t *testing.T) { | ||
| var _ pb.ResourceDriverServer = (*resourceDriverStub)(nil) | ||
| } |
Comment on lines
+203
to
+211
| // DriftClass mirrors interfaces.DriftClass. Wire-stable string-equivalent | ||
| // enum: zero value is UNKNOWN (matches Go's "" zero value), with explicit | ||
| // IN_SYNC / GHOST / CONFIG variants. | ||
| enum DriftClass { | ||
| DRIFT_CLASS_UNKNOWN = 0; | ||
| DRIFT_CLASS_IN_SYNC = 1; | ||
| DRIFT_CLASS_GHOST = 2; | ||
| DRIFT_CLASS_CONFIG = 3; | ||
| } |
3 tasks
⏱ Benchmark Results✅ No significant performance regressions detected. benchstat comparison (baseline → PR)
|
…tion Three review-feedback fixes for PR 598 (Task 3 iac.proto), per team-lead's follow-up directive on the test that silently followed the proto: - Important-1: rewrite TestIaCProviderRequiredServerHasAllRequiredMethods and TestResourceDriverServerInterfaceExists to use locally-enumerated method-signature interfaces (`iacRequiredMethodsCheck`, `resourceDriverMethodsCheck`) and assert pb.X(nil) satisfies them. Drop an RPC from iac.proto + regenerate, and these blank assertions fail to compile — the previous form would silently follow the proto regen. - Minor-2: rewrite the DriftClass enum comment in iac.proto with an explicit translation table (DRIFT_CLASS_UNKNOWN ↔ "" / IN_SYNC ↔ "in-sync" / GHOST ↔ "ghost" / CONFIG ↔ "config") and a marshal-boundary warning. Regenerated iac.pb.go picks up the new doc. - Minor-4: add TestMigrationRepairConfirmationStringMatchesProtoComment to guard interfaces.MigrationRepairConfirmation == "FORCE_MIGRATION_METADATA" against drift from the proto comment. Verified locally: `GOWORK=off go test ./plugin/external/proto/... -count=1` => ok 0.301s.
Synthetic failure commit per Minor-3: temporarily delete the Status RPC
from iac.proto + regenerate, to demonstrate the new locally-enumerated
iacRequiredMethodsCheck guard catches RPC drops at compile time.
Captured failure:
plugin/external/proto/iac_proto_test.go:34:33: cannot use
(pb.IaCProviderRequiredServer)(nil) (value of interface type
"github.com/GoCodeAlone/workflow/plugin/external/proto".IaCProviderRequiredServer)
as iacRequiredMethodsCheck value in variable declaration:
IaCProviderRequiredServer does not implement iacRequiredMethodsCheck
(missing method Status)
The next commit restores the Status RPC and confirms the guard goes
green again. This intermediate commit is intentionally broken; do not
bisect across it.
NOTE: this commit deliberately ships a failing test as evidence that
the guard works. CI on this commit's tip will FAIL by design.
Restores the Status RPC dropped in the previous synthetic-failure demo commit. After regeneration, the iacRequiredMethodsCheck guard is satisfied again: $ GOWORK=off go test ./plugin/external/proto/... -count=1 ok github.com/GoCodeAlone/workflow/plugin/external/proto 0.314s Together the prior commit and this one demonstrate that the new test catches RPC drops at compile time — landed end-state for PR 598 is identical to the iac.proto baseline + the Important-1/Minor-2/Minor-4 fixes from commit 8641675.
intel352
added a commit
that referenced
this pull request
May 10, 2026
…CProviderRequiredClient (Task 16) See above commit body in this branch's PR description for full detail. Summary: - discoverAndLoadIaCProvider rewritten to construct typedIaCAdapter from adapter.Conn() + ContractRegistry-derived registered service map. - DELETED ~3856 lines: remoteIaCProvider, remoteResourceDriver, remoteServiceInvoker/ContextInvoker, jsonToAny, anyToStruct, sensitiveToAny, decodeResourceOutput, isPluginMethodUnimplemented, stringVal, stringFromMap, loadIaCPlugin var, defaultLoadIaCPlugin, readIaCPluginComputePlanVersion, plus 5 dependent test files (~2767 lines) and the loadIaCPlugin-using TestResolveIaCProviderSurfacesPluginError. - KEPT wrapIaCError + retryOnTransient + deployOpError as provider-agnostic helpers used by pluginDeployProvider against typed RPC errors. - ADDED registeredIaCServices(reg) helper. DEPENDENCY: branch CI red until PRs #598 (Task 3 proto) + #605 (Task 30 adapter) merge. Local validation passes against working-tree overlay of both: build clean, vet clean, ./cmd/wfctl/... -short PASS. Plan-correction notes (per Path A ruling): Conn() prerequisite shipped as commit ad7d946 in this same PR. SupportedCanonicalKeys regression acceptable transient (closes via follow-up additive PR per team-lead ruling). ComputePlanVersionDeclarer regression tracked for follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
intel352
added a commit
that referenced
this pull request
May 10, 2026
Task 4 of the strict-contracts force-cutover plan (docs/plans/2026-05-10-strict-contracts-force-cutover.md, rev5). Adds plugin/external/sdk/iacserver.go: a single helper that uses Go type-assertion to register every typed IaC gRPC service the provider satisfies, in one call. REQUIRED service: pb.IaCProviderRequiredServer — surfaced as a clear startup-time error if the provider type doesn't satisfy it (rather than failing at the first RPC dispatch with a generic "unimplemented" status). OPTIONAL services (auto-detected): IaCProviderEnumerator, IaCProviderDriftDetector, IaCProviderCredentialRevoker, IaCProviderMigrationRepairer, IaCProviderValidator, IaCProviderDriftConfigDetector. Plus ResourceDriver. Per cycle 3 I-1 of the design: plugin authors write ONE call; they cannot omit registration for a capability they implemented. This removes the registration-omission bug class (the same shape as the legacy InvokeService case-string-typo bug) by removing the manual step entirely. Tests cover four cases: - required-satisfied → required service registered + advertised by grpcSrv.GetServiceInfo(). - enumerator-only → only the optional Enumerator service registered; other optionals stay absent (auto-detection precision). - empty-stub → returns an error naming the unsatisfied required interface, with a docs pointer. - all-capabilities-stub → all 8 typed services (Required + 6 optional + ResourceDriver) registered. Stacked on feat/iac-proto-task3 (Task 3 PR #598 provides the generated server interfaces this helper consumes). Verification: GOWORK=off go test -race ./plugin/external/sdk/... PASS; GOWORK=off go build ./plugin/... ./cmd/... ./module/... clean; GOWORK=off go vet ./plugin/external/... clean. Rollback: revert this commit; SDK consumers can still register services manually via the per-service Register* helpers protoc generated.
intel352
added a commit
that referenced
this pull request
May 10, 2026
…k 30)
Adds cmd/wfctl/iac_typed_adapter.go: a thin adapter that wraps the
typed pb.IaC* gRPC clients (Task 3) and satisfies the existing Go
interfaces.IaCProvider plus every relevant optional sub-interface
(Enumerator, EnumeratorAll, DriftConfigDetector, ProviderValidator,
ProviderCredentialRevoker, ProviderMigrationRepairer). Engine
consumers (module/infra_module.go, iac/wfctlhelpers/apply.go, etc.)
keep calling interfaces.IaCProvider methods unchanged; the adapter
translates each call to a typed RPC on the underlying client. No
string dispatch, no map[string]any crossing the wire — provider-
specific free-form payloads carry as JSON bytes per the proto
§config_json / outputs_json design so the engine boundary stays
strongly typed without ossifying provider-specific shapes.
Per ADR-0026 (Task 14): this is NOT a hand-written marshalling proxy
of the kind the legacy remoteIaCProvider was. Each Go-interface
method maps 1:1 to a typed RPC. Optional sub-interfaces are always
satisfied at the Go type level (so v0.27.1 type-assert sites
continue to compile) but return interfaces.ErrProviderMethodUnimplemented
at call time when the underlying optional service was never
registered by the plugin — preserving the iterate-and-skip
semantics dispatch sites already implement.
ResourceDriver returns a typedResourceDriver wrapper that carries
the resource_type on every RPC, matching the DO plugin's 14-driver
type-routing pattern in Task 11.
API surface:
- typedIaCAdapter — interfaces.IaCProvider + all relevant
optional sub-interfaces.
- typedResourceDriver — interfaces.ResourceDriver +
Troubleshooter, parameterised by
resource_type.
- newTypedIaCAdapter(conn, registered) — required client always
wired; optional clients gated by
fully-qualified service-name keys
from the plugin's ContractRegistry.
- translateRPCErr / unimplementedOptional — sentinel translation so
callers continue to errors.Is on
interfaces.ErrProviderMethodUnimplemented.
DEPENDENCY: this PR depends on Task 3 (PR #598 — adds
plugin/external/proto/iac.{proto,pb.go,_grpc.pb.go}). CI on this
branch will FAIL until PR #598 merges and this branch is rebased
onto the resulting main. Per team-lead direction the branch base is
main (not stacked on PR #598) so the cutover stays linear.
Local validation against feat/iac-proto-task3 (proto + sdk landed
in working tree):
GOWORK=off go vet ./cmd/wfctl/ → clean
GOWORK=off go build ./cmd/wfctl/ → clean
GOWORK=off go test ./cmd/wfctl/ -count=1 -short → all PASS
GOWORK=off go test ./cmd/wfctl/ -run TestTypedAdapter
→ 6 PASS (interface conformance, optional sentinel,
ValidatePlan-absent, DriftClass round-trip,
codes.Unimplemented translation, end-to-end
Name/Version/EnumerateAll over in-process gRPC).
Rollback: revert this commit. The legacy remoteIaCProvider proxy
remains unchanged and continues to satisfy interfaces.IaCProvider —
Task 16 is the one that swaps the wfctl call site over.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
intel352
added a commit
that referenced
this pull request
May 10, 2026
…ask 4) [reopens #599] (#611) * feat(sdk): RegisterAllIaCProviderServices auto-registration helper Task 4 of the strict-contracts force-cutover plan (docs/plans/2026-05-10-strict-contracts-force-cutover.md, rev5). Adds plugin/external/sdk/iacserver.go: a single helper that uses Go type-assertion to register every typed IaC gRPC service the provider satisfies, in one call. REQUIRED service: pb.IaCProviderRequiredServer — surfaced as a clear startup-time error if the provider type doesn't satisfy it (rather than failing at the first RPC dispatch with a generic "unimplemented" status). OPTIONAL services (auto-detected): IaCProviderEnumerator, IaCProviderDriftDetector, IaCProviderCredentialRevoker, IaCProviderMigrationRepairer, IaCProviderValidator, IaCProviderDriftConfigDetector. Plus ResourceDriver. Per cycle 3 I-1 of the design: plugin authors write ONE call; they cannot omit registration for a capability they implemented. This removes the registration-omission bug class (the same shape as the legacy InvokeService case-string-typo bug) by removing the manual step entirely. Tests cover four cases: - required-satisfied → required service registered + advertised by grpcSrv.GetServiceInfo(). - enumerator-only → only the optional Enumerator service registered; other optionals stay absent (auto-detection precision). - empty-stub → returns an error naming the unsatisfied required interface, with a docs pointer. - all-capabilities-stub → all 8 typed services (Required + 6 optional + ResourceDriver) registered. Stacked on feat/iac-proto-task3 (Task 3 PR #598 provides the generated server interfaces this helper consumes). Verification: GOWORK=off go test -race ./plugin/external/sdk/... PASS; GOWORK=off go build ./plugin/... ./cmd/... ./module/... clean; GOWORK=off go vet ./plugin/external/... clean. Rollback: revert this commit; SDK consumers can still register services manually via the per-service Register* helpers protoc generated. * fix(sdk): reject typed-nil pointer in RegisterAllIaCProviderServices Per cycle 4 code-review PR 611 typed-nil hardening (Copilot finding): The previous `if provider == nil` check missed the typed-nil case. A typed-nil pointer wrapped in `any` (e.g. `var p *MyProvider; sdk.RegisterAllIaCProviderServices(s, p)`) carries a non-nil interface header (type-info present), so the bare nil-check passes. The subsequent `provider.(pb.IaCProviderRequiredServer)` type assertion ALSO succeeds (the type satisfies the interface). Registration proceeds with the typed-nil receiver. First RPC dispatch panics on the nil pointer dereference. Adds a reflect-based check that rejects typed-nil pointers BEFORE any registration happens, with the same error shape as the bare nil-check (plus the underlying type for actionable diagnostic). Test (TestRegisterAllIaCProviderServices_TypedNilPointer_ReturnsError) exercises the failure mode end-to-end: a typed-nil *fullProviderStub must produce an error naming "typed-nil" + zero registered services. Verification: GOWORK=off go test -race ./plugin/external/sdk/... -run TestRegisterAllIaCProvider → PASS (5/5); gofmt clean. Rollback: revert this commit; the bare nil-check returns and typed-nil panics at first RPC dispatch (the bug class Copilot caught). * fix(sdk): use reflect.Pointer (not deprecated reflect.Ptr alias) Per cycle 4 code-review PR 611 lint fix: govet's `inline` analyzer flagged reflect.Ptr as the deprecated-alias name. Go 1.18+ canonical is reflect.Pointer; reflect.Ptr is retained for backward compat but should not be used in new code. One-character substitution. No semantic change. Verification: GOWORK=off go test ./plugin/external/sdk/ -run TestRegisterAllIaCProvider -count=1 → PASS (5/5); golangci-lint run ./plugin/external/sdk/... → 0 issues.
intel352
added a commit
that referenced
this pull request
May 10, 2026
…4 / Task 30) (#605) * feat(wfctl): typed-IaC adapter satisfying interfaces.IaCProvider (Task 30) Adds cmd/wfctl/iac_typed_adapter.go: a thin adapter that wraps the typed pb.IaC* gRPC clients (Task 3) and satisfies the existing Go interfaces.IaCProvider plus every relevant optional sub-interface (Enumerator, EnumeratorAll, DriftConfigDetector, ProviderValidator, ProviderCredentialRevoker, ProviderMigrationRepairer). Engine consumers (module/infra_module.go, iac/wfctlhelpers/apply.go, etc.) keep calling interfaces.IaCProvider methods unchanged; the adapter translates each call to a typed RPC on the underlying client. No string dispatch, no map[string]any crossing the wire — provider- specific free-form payloads carry as JSON bytes per the proto §config_json / outputs_json design so the engine boundary stays strongly typed without ossifying provider-specific shapes. Per ADR-0026 (Task 14): this is NOT a hand-written marshalling proxy of the kind the legacy remoteIaCProvider was. Each Go-interface method maps 1:1 to a typed RPC. Optional sub-interfaces are always satisfied at the Go type level (so v0.27.1 type-assert sites continue to compile) but return interfaces.ErrProviderMethodUnimplemented at call time when the underlying optional service was never registered by the plugin — preserving the iterate-and-skip semantics dispatch sites already implement. ResourceDriver returns a typedResourceDriver wrapper that carries the resource_type on every RPC, matching the DO plugin's 14-driver type-routing pattern in Task 11. API surface: - typedIaCAdapter — interfaces.IaCProvider + all relevant optional sub-interfaces. - typedResourceDriver — interfaces.ResourceDriver + Troubleshooter, parameterised by resource_type. - newTypedIaCAdapter(conn, registered) — required client always wired; optional clients gated by fully-qualified service-name keys from the plugin's ContractRegistry. - translateRPCErr / unimplementedOptional — sentinel translation so callers continue to errors.Is on interfaces.ErrProviderMethodUnimplemented. DEPENDENCY: this PR depends on Task 3 (PR #598 — adds plugin/external/proto/iac.{proto,pb.go,_grpc.pb.go}). CI on this branch will FAIL until PR #598 merges and this branch is rebased onto the resulting main. Per team-lead direction the branch base is main (not stacked on PR #598) so the cutover stays linear. Local validation against feat/iac-proto-task3 (proto + sdk landed in working tree): GOWORK=off go vet ./cmd/wfctl/ → clean GOWORK=off go build ./cmd/wfctl/ → clean GOWORK=off go test ./cmd/wfctl/ -count=1 -short → all PASS GOWORK=off go test ./cmd/wfctl/ -run TestTypedAdapter → 6 PASS (interface conformance, optional sentinel, ValidatePlan-absent, DriftClass round-trip, codes.Unimplemented translation, end-to-end Name/Version/EnumerateAll over in-process gRPC). Rollback: revert this commit. The legacy remoteIaCProvider proxy remains unchanged and continues to satisfy interfaces.IaCProvider — Task 16 is the one that swaps the wfctl call site over. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(wfctl): preserve gRPC status in unwrap chain + log Name/Version RPC failures (PR 605 MINORs) Code-review feedback on PR #605 (Task 30): MINOR-1 (Copilot) — translateRPCErr wrapped with `%w/%s` (err.Error() flattened to a string), so callers walking the unwrap chain via status.FromError lost the gRPC status code and details. Switch to `%w/%w` so the original *status.Error stays in the chain — retry classifiers that distinguish codes.Unimplemented vs codes.Unavailable recover the signal correctly. Test addition: TestTypedAdapter_TranslateRPCErrSurfacesUnimplemented now asserts `status.FromError(translated).Code() == codes.Unimplemented` in addition to the existing errors.Is sentinel check. MINOR-2 (code-reviewer) — Name() / Version() silently swallowed RPC errors via `return ""`, indistinguishable from intentional empty. The Go interface signatures `Name() string` / `Version() string` permit no error return, so we cannot fix the contract; mitigate by log.Printf on the failure path so operators have a trail when troubleshooting "why is my provider nameless." Adds a top-of-section comment documenting the design constraint. Local validation (against feat/iac-proto-task3 working-tree overlay): GOWORK=off go test ./cmd/wfctl/ -run TestTypedAdapter -count=1 → all PASS (1.5s) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(wfctl): address PR 605 lint + map-copy MINORs Code-reviewer feedback on PR #605 (Task 30): 5 lint failures + 1 Copilot MINOR, all in cmd/wfctl/iac_typed_adapter.go. All addressed in one commit: **rangeValCopy (gocritic)** at lines 741, 963 — `interfaces.ResourceState` (~240 bytes) and `interfaces.PlanAction` (~152 bytes) were value-copied per iteration. Switched to index iteration + pointer pass-through: `for i := range states { stateToPB(&states[i]) }`. Required updating `stateToPB` and `planActionToPB` signatures from value to pointer receiver — both are package-private, callers updated. **G115 int→int32 narrowing (gosec)** at lines 516, 975, 1094 — 3 distinct sites: - typedResourceDriver.Scale: `replicas int → int32` is operator-supplied (CLI flag); added math.MinInt32/MaxInt32 bounds check that returns a typed error before the narrowing. - planToPB: `p.SchemaVersion int → int32` — same bounds check + typed error return (planToPB already returns error). - migrationRepairRequestToPB: `r.TimeoutSeconds int → int32` is also operator-supplied; clamped to [0, MaxInt32] (non-error fallback since the helper signature does NOT return error). Real-world values are seconds-scale, well within int32; clamp is defensive. - All three use `//nolint:gosec // G115: range-checked above` (or `clamped above`) on the actual conversion to keep gosec quiet about the now-safe cast. **Copilot MINOR (BootstrapStateBackend, line 306)** — `r.GetEnvVars()` exposed the proto's underlying map; callers could mutate proto-internal state. Switched to `copyStringMap(r.GetEnvVars())` (the helper this file already uses for `Env` in MigrationRepairRequest and `InputSnapshot` in IaCPlan), nil-safe. Local validation: GOWORK=off go build ./cmd/wfctl/ # clean GOWORK=off go vet ./cmd/wfctl/ # clean GOWORK=off go test ./cmd/wfctl/ -run TestTypedAdapter -count=1 # all PASS (1.5s) GOWORK=off golangci-lint run --enable=gocritic,gosec ./cmd/wfctl/... # 0 issues Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
intel352
added a commit
that referenced
this pull request
May 10, 2026
…CProviderRequiredClient (Task 16) See above commit body in this branch's PR description for full detail. Summary: - discoverAndLoadIaCProvider rewritten to construct typedIaCAdapter from adapter.Conn() + ContractRegistry-derived registered service map. - DELETED ~3856 lines: remoteIaCProvider, remoteResourceDriver, remoteServiceInvoker/ContextInvoker, jsonToAny, anyToStruct, sensitiveToAny, decodeResourceOutput, isPluginMethodUnimplemented, stringVal, stringFromMap, loadIaCPlugin var, defaultLoadIaCPlugin, readIaCPluginComputePlanVersion, plus 5 dependent test files (~2767 lines) and the loadIaCPlugin-using TestResolveIaCProviderSurfacesPluginError. - KEPT wrapIaCError + retryOnTransient + deployOpError as provider-agnostic helpers used by pluginDeployProvider against typed RPC errors. - ADDED registeredIaCServices(reg) helper. DEPENDENCY: branch CI red until PRs #598 (Task 3 proto) + #605 (Task 30 adapter) merge. Local validation passes against working-tree overlay of both: build clean, vet clean, ./cmd/wfctl/... -short PASS. Plan-correction notes (per Path A ruling): Conn() prerequisite shipped as commit ad7d946 in this same PR. SupportedCanonicalKeys regression acceptable transient (closes via follow-up additive PR per team-lead ruling). ComputePlanVersionDeclarer regression tracked for follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
intel352
added a commit
that referenced
this pull request
May 10, 2026
…CProviderRequiredClient (PR 4 / Task 16) (#609) * feat(plugin/external): expose grpc.ClientConn via PluginClient.Conn() / ExternalPluginAdapter.Conn() Architectural prerequisite for plan §Task 16 (wfctl typed-IaC cutover). The current PluginClient drops the *grpc.ClientConn after constructing its embedded pb.PluginServiceClient, so callers had no way to build additional typed gRPC service clients (e.g. pb.IaCProviderRequiredClient, pb.ResourceDriverClient) against the same plugin process. The legacy remoteIaCProvider sidestepped this by routing every call through PluginServiceClient.InvokeService string-dispatch — which is the bug class force-cutover Task 16 is closing. Surface (additive — zero call-site impact): - `PluginClient.conn *grpc.ClientConn` — retained from GRPCPlugin GRPCClient construction. - `(p *PluginClient) Conn() *grpc.ClientConn` — opaque accessor; exposed via method (not public field) so the rest of PluginClient stays internal. - `(a *ExternalPluginAdapter) Conn() *grpc.ClientConn` — delegates to client.Conn(); nil-safe for adapters constructed via `newExternalPluginAdapterWithContractRegistry` (test fixtures). The connection lifecycle is owned by the host's plugin manager — callers MUST NOT Close() the returned conn. The plugin shutdown path tears it down via the registered Closer; closing it externally would break every other typed-client constructed against the same process. ## Plan-correction notes This commit is NOT in plan §Task 16 Files: section (spec gap): - Spec assumes the typed pb.IaCProviderRequiredClient can be constructed from the existing plugin loader output, but the plugin/external surface as it stands strips the underlying conn. Task 16 is physically impossible without first exposing it. Per scope-lock skill the prerequisite lands in the SAME PR as the Task 16 cutover (this branch) rather than as a separate PR — same precedent as Task 2's plan-correction notes block. Documented in the PR description. Local validation: GOWORK=off go build ./plugin/external/... → clean GOWORK=off go vet ./plugin/external/... → clean GOWORK=off go test ./plugin/external/... -count=1 -short → all PASS Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(wfctl): typed-IaC cutover — replace remoteIaCProvider with pb.IaCProviderRequiredClient (Task 16) See above commit body in this branch's PR description for full detail. Summary: - discoverAndLoadIaCProvider rewritten to construct typedIaCAdapter from adapter.Conn() + ContractRegistry-derived registered service map. - DELETED ~3856 lines: remoteIaCProvider, remoteResourceDriver, remoteServiceInvoker/ContextInvoker, jsonToAny, anyToStruct, sensitiveToAny, decodeResourceOutput, isPluginMethodUnimplemented, stringVal, stringFromMap, loadIaCPlugin var, defaultLoadIaCPlugin, readIaCPluginComputePlanVersion, plus 5 dependent test files (~2767 lines) and the loadIaCPlugin-using TestResolveIaCProviderSurfacesPluginError. - KEPT wrapIaCError + retryOnTransient + deployOpError as provider-agnostic helpers used by pluginDeployProvider against typed RPC errors. - ADDED registeredIaCServices(reg) helper. DEPENDENCY: branch CI red until PRs #598 (Task 3 proto) + #605 (Task 30 adapter) merge. Local validation passes against working-tree overlay of both: build clean, vet clean, ./cmd/wfctl/... -short PASS. Plan-correction notes (per Path A ruling): Conn() prerequisite shipped as commit ad7d946 in this same PR. SupportedCanonicalKeys regression acceptable transient (closes via follow-up additive PR per team-lead ruling). ComputePlanVersionDeclarer regression tracked for follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(wfctl): PR #609 review fixes — Copilot + Step 1 boundary test Code-review feedback on PR #609 (Task 16) and post-rebase consolidation after PR #605 merged. Addresses 4 of 6 Copilot findings + the IMPORTANT spec-reviewer Step 1 test gap. Fix #1 (Task 18 wiring) deferred until PR #610 merges (predicate not yet on main); Fix #5 (test file comment) already cleaned up post-rebase. Fix #6 (computePlanVersion stale return) documented in-place per team-lead recommendation (avoids 2-cycle churn when the follow-up capability-extension PR wires it back in). **Copilot fix #1 — `plugin/external/adapter.go:Conn()` doc broadened.** The previous doc claimed `nil` only for adapters constructed without a backing PluginClient (test fixtures). It can ALSO return nil when the PluginClient is non-nil but its underlying *grpc.ClientConn is nil (in-process test plumbing wiring only the PluginServiceClient interface without a real conn). Doc now enumerates both cases. **Copilot fix #3 — `cmd/wfctl/deploy_providers.go` surfaces ContractRegistryError.** The previous loader path called `registeredIaCServices(adapter.ContractRegistry())` without first checking `adapter.ContractRegistryError()`. A transport- level RPC failure (codes.Unimplemented from a legacy plugin, transient network reset, etc.) silently degraded to an empty registry, then the next `if !registered[iacServiceRequired]` branch fired the misleading "does not register the required service" error — masking the real cause. Now: surface ContractRegistryError() FIRST with `wfctl plugin update` hint; fall through to the registration-check only when the RPC succeeded. Test coverage: `TestDiscoverAndLoadIaCProvider_SurfacesContractRegistryError`. **Copilot fix #6 — `findIaCPluginDir.computePlanVersion` documented as reserved for follow-up.** discoverAndLoadIaCProvider no longer reads the value (the legacy reader `readIaCPluginComputePlanVersion` was deleted with remoteIaCProvider). Per team-lead: leave the return in place rather than churning the signature now; a follow-up PR adds `compute_plan_version` to `CapabilitiesResponse.IaCCapabilityDeclaration` (option (d), batched with canonical_keys between Task 17 and Task 20) and wires it back in via the typed Capabilities RPC. In-line comment documents the reservation + the follow-up plan. **IMPORTANT spec-reviewer Fix 2 — Step 1 boundary test added.** New file `cmd/wfctl/discover_typed_loader_test.go` extracts a unit- testable seam `buildTypedIaCAdapterFrom(adapter)` from the loader's post-LoadPlugin half (factored out of discoverAndLoadIaCProvider with the `iacAdapterAccessor` interface so tests don't pay the subprocess cost). Three boundary tests: - TestDiscoverAndLoadIaCProvider_ReturnsTypedClient — asserts the cutover invariant: loader returns `*typedIaCAdapter`, NOT the legacy `*remoteIaCProvider` (which no longer compiles post-cutover). In-process gRPC server with a stub IaCProviderRequiredServer + Initialize-only response. - TestDiscoverAndLoadIaCProvider_RejectsMissingRequiredService — asserts the strict-contracts hard-cutover invariant: plugins whose ContractRegistry omits `IaCProviderRequired` are rejected at load time with an actionable `wfctl plugin update` hint. Verifies message contract for operator UX. - TestDiscoverAndLoadIaCProvider_SurfacesContractRegistryError — asserts Copilot fix #3 above; transport-level ContractRegistry failure is surfaced via errors.Is + RPC-failure framing. **Test-file comment refresh** in deploy_providers_test.go cite of iac_typed_adapter_test.go updated post-rebase: file now lives on main via PR #605 (no longer "not present in this PR"). Also dropped the unused `noopCloser` helper that lint flagged after the legacy TestResolveIaCProviderSurfacesPluginError removal. **Cutover dependency status:** PR #605 (typed adapter) + PR #611 (sdk auto-register) MERGED to main; rebase clean. PR #610 (Task 18 loader gate) still pending — Fix 1 (replace inline gate with AssertIaCPluginAdvertisesRequiredService predicate) lands in a follow-up commit on this branch once PR #610 merges and I rebase again. Const naming aligned via impl-3's PR #610 rename (`iacServiceRequired` is canonical across both files). Local validation: GOWORK=off go build ./... → clean GOWORK=off go vet ./cmd/wfctl/ ./plugin/external/... → clean GOWORK=off go test ./cmd/wfctl/ -count=1 -short → all PASS (7.0s) GOWORK=off go test ./cmd/wfctl/ -run TestDiscoverAndLoadIaCProvider -count=1 → 3 PASS (1.5s) GOWORK=off golangci-lint run --enable=gocritic,gosec ./cmd/wfctl/... → 0 issues Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
intel352
added a commit
that referenced
this pull request
May 10, 2026
…e (Task 6) (#603) * feat(proto): add iac.proto with IaCProviderRequired + 6 optional services + ResourceDriver Task 3 of the strict-contracts force-cutover plan (docs/plans/2026-05-10-strict-contracts-force-cutover.md, rev5). Adds plugin/external/proto/iac.proto defining the typed gRPC contract that supersedes the legacy InvokeService/structpb dispatch path for IaCProvider + ResourceDriver: - service IaCProviderRequired: 11 RPCs every IaC plugin MUST implement (Initialize, Name, Version, Capabilities, Plan, Apply, Destroy, Status, Import, ResolveSizing, BootstrapStateBackend). Compile-time enforced via the SDK type-assert in Task 4. - 6 optional services — providers register only the ones they support: IaCProviderEnumerator (EnumerateAll, EnumerateByTag), IaCProviderDriftDetector (DetectDrift, DetectDriftWithSpecs), IaCProviderCredentialRevoker (RevokeProviderCredential), IaCProviderMigrationRepairer (RepairDirtyMigration), IaCProviderValidator (ValidatePlan), IaCProviderDriftConfigDetector (DetectDriftConfig). Absence of registration IS the negative signal — no NotSupported field on any optional response (per design §Optional services). - service ResourceDriver: 9 RPCs for per-resource-type CRUD dispatch (Create, Read, Update, Delete, Diff, Scale, HealthCheck, SensitiveKeys, Troubleshoot), each carrying resource_type so a single server can route to the per-type driver implementation. Hard invariants honored: - NO google.protobuf.Struct, NO google.protobuf.Any anywhere. - Free-form per-resource Config/Outputs payloads cross the wire as bytes <name>_json (the plugin owns json.Marshal/Unmarshal); this eliminates the structpb conversion surface that previously dropped map[string]bool entries silently (T3.9 finding). - ResourceOutput.sensitive uses typed map<string, bool> per design. Generated iac.pb.go + iac_grpc.pb.go via protoc v34.1 + protoc-gen-go v1.36.11 + protoc-gen-go-grpc v1.6.1. Failing test (plugin/external/proto/iac_proto_test.go) asserts the generated server interfaces exist and have the methods the design requires — drops in iac.proto cause the test file to fail to compile. Verification: GOWORK=off go test ./plugin/external/proto/... PASSES; GOWORK=off go build ./plugin/... ./cmd/... ./module/... clean. Rollback: revert this commit; legacy InvokeService dispatch in plugin.proto remains functional; the additive-only nature of this PR means no consumer is affected until subsequent tasks wire callers. * feat(sdk): RegisterAllIaCProviderServices auto-registration helper Task 4 of the strict-contracts force-cutover plan (docs/plans/2026-05-10-strict-contracts-force-cutover.md, rev5). Adds plugin/external/sdk/iacserver.go: a single helper that uses Go type-assertion to register every typed IaC gRPC service the provider satisfies, in one call. REQUIRED service: pb.IaCProviderRequiredServer — surfaced as a clear startup-time error if the provider type doesn't satisfy it (rather than failing at the first RPC dispatch with a generic "unimplemented" status). OPTIONAL services (auto-detected): IaCProviderEnumerator, IaCProviderDriftDetector, IaCProviderCredentialRevoker, IaCProviderMigrationRepairer, IaCProviderValidator, IaCProviderDriftConfigDetector. Plus ResourceDriver. Per cycle 3 I-1 of the design: plugin authors write ONE call; they cannot omit registration for a capability they implemented. This removes the registration-omission bug class (the same shape as the legacy InvokeService case-string-typo bug) by removing the manual step entirely. Tests cover four cases: - required-satisfied → required service registered + advertised by grpcSrv.GetServiceInfo(). - enumerator-only → only the optional Enumerator service registered; other optionals stay absent (auto-detection precision). - empty-stub → returns an error naming the unsatisfied required interface, with a docs pointer. - all-capabilities-stub → all 8 typed services (Required + 6 optional + ResourceDriver) registered. Stacked on feat/iac-proto-task3 (Task 3 PR #598 provides the generated server interfaces this helper consumes). Verification: GOWORK=off go test -race ./plugin/external/sdk/... PASS; GOWORK=off go build ./plugin/... ./cmd/... ./module/... clean; GOWORK=off go vet ./plugin/external/... clean. Rollback: revert this commit; SDK consumers can still register services manually via the per-service Register* helpers protoc generated. * feat(sdk): ServeIaCPlugin high-level entrypoint with go-plugin GRPCServer callback Task 29 of the strict-contracts force-cutover plan (docs/plans/2026-05-10-strict-contracts-force-cutover.md, rev5). Adds the high-level plugin-author API on top of Task 4's RegisterAllIaCProviderServices: func main() { sdk.ServeIaCPlugin(&doProvider{}, sdk.IaCServeOptions{}) } Per cycle 3 I-1 of the design, service registration happens INSIDE go-plugin's GRPCServer callback (iacGRPCPlugin.GRPCServer) — the framework owns *grpc.Server lifecycle, so plugin authors cannot pre-create a server and forget to register a typed service on it. API surface (all in plugin/external/sdk/iacserver.go): - IaCServeOptions{ PluginInfo *PluginInfo } — caller-side options. - PluginInfo{ HandshakeConfig goplugin.HandshakeConfig } — extension point for future Name/Version metadata; defaults to ext.Handshake (the canonical wfctl<->plugin handshake) when zero-valued. - iacGRPCPlugin{provider any} — implements goplugin.Plugin (GRPCServer + GRPCClient). The GoCodeAlone fork of go-plugin v1.7.0 is gRPC-only and exposes only the canonical Plugin interface; there is no GRPCPlugin alias or NetRPCUnsupportedPlugin embed to use. - ServeIaCPlugin(provider, opts) — wraps goplugin.Serve with the resolved handshake + a single iacGRPCPlugin entry under the "iac" key. - resolveServeHandshake(opts) — extracted helper so the override-vs- default rule is unit-testable without invoking the blocking goplugin.Serve loop. Tests (iacserver_serve_test.go) cover six cases via internal-package tests (so the unexported plugin type is exercisable without a real subprocess; subprocess-level coverage lands in Task 6's typed-IaC E2E test): - iacGRPCPlugin.GRPCServer registers all satisfied services on the framework-managed *grpc.Server (Required + Enumerator + ResourceDriver for the all-stub). - iacGRPCPlugin.GRPCServer propagates the auto-register error for an empty stub — go-plugin aborts plugin startup with an actionable message. - iacGRPCPlugin.GRPCClient is a no-op (host builds typed clients directly). - iacGRPCPlugin satisfies goplugin.Plugin at compile time (refactor guard). - ServeIaCPlugin defaults to ext.Handshake when PluginInfo is nil. - ServeIaCPlugin honors a non-zero override handshake when provided. Stacked on feat/iac-sdk-auto-register-task4 (Task 4 PR #599 provides RegisterAllIaCProviderServices, which the GRPCServer callback delegates to). Verification: GOWORK=off go test -race ./plugin/external/sdk/... PASS; GOWORK=off go build ./plugin/... ./cmd/... ./module/... clean; GOWORK=off go vet ./plugin/external/... clean. Rollback: revert this commit; plugin authors can fall back to manually constructing goplugin.Serve + Plugins map referencing RegisterAllIaCProviderServices in their own GRPCServer callback. * feat(sdk): BuildContractRegistry advertises registered IaC services Task 5 of the strict-contracts force-cutover plan (docs/plans/2026-05-10-strict-contracts-force-cutover.md, rev5). Adds plugin/external/sdk/contracts.go with the BuildContractRegistry helper that enumerates grpc.Server.GetServiceInfo() and emits a SERVICE-kind ContractDescriptor for each registered service. ContractMode is set to STRICT_PROTO so the host can distinguish typed IaC services from the legacy structpb-mode contracts produced by Module/Step/Trigger ContractProvider implementations. Per cycle 3 I-1 of the design: wfctl needs a single mechanism to discover "is the optional service registered on this plugin handle?". Reusing the existing ContractRegistry shape keeps Module/Step/Trigger and IaC capability discovery on the same wire surface — no new gRPC server-reflection dependency required. Service descriptors are emitted in deterministic alphabetical order so callers can rely on stable output for diff/compare operations and the wftest BDD test in Task 15. The helper is safe to call with a nil server (returns an empty but non-nil ContractRegistry) so callers that may construct it before the gRPC server exists do not panic. Tests (contracts_iac_test.go) cover three cases — all pass: - AdvertisesRegisteredIaCServices: a Required + Enumerator + DriftDetector stub yields exactly those service descriptors. - ServiceContractsUseStrictProtoMode: every emitted descriptor is Kind=SERVICE + Mode=STRICT_PROTO (host-side discriminator). - NilServer_ReturnsEmpty: defensive contract for nil input. Stacked on feat/iac-sdk-serve-task29 (Task 29 PR #600 provides ServeIaCPlugin which IaC plugins use to register the services this helper enumerates). Verification: GOWORK=off go test -race ./plugin/external/sdk/... PASS; GOWORK=off go build ./plugin/... ./cmd/... ./module/... clean; GOWORK=off go vet ./plugin/external/... clean. Rollback: revert this commit; ContractRegistry returns the prior shape (Module/Step/Trigger only via the existing ContractProvider hook in grpc_server.go). * test(sdk): typed-IaC E2E integration test + cross-plugin-build CI gate Task 6 of the strict-contracts force-cutover plan (docs/plans/2026-05-10-strict-contracts-force-cutover.md, rev5). Adds plugin/external/sdk/iac_e2e_test.go (build tag `integration`) — the canonical workflow-side smoke test for the typed IaC contract. Uses bufconn for in-process gRPC, registers a fake provider via sdk.RegisterAllIaCProviderServices, dials the server through a real gRPC channel, and exercises typed RPCs on both Required (Name, Version) and the Enumerator optional (EnumerateAll). Critical assertion: ResourceOutput.Sensitive (typed map<string,bool>) survives the roundtrip with value=true. The pre-cutover structpb path silently dropped this map (T3.9 finding); this E2E test guards the regression. Second case asserts that when a provider satisfies Required ONLY (no Enumerator embed), the auto-registration helper SKIPS the optional service registration — and a typed enumerator client receives a gRPC-layer Unimplemented error rather than a NotSupported flag in a response body. This is the "absence of registration IS the negative signal" contract from the design. CI integration (.github/workflows/cross-plugin-build-test.yml): - Adds an `iac-typed-e2e` job that runs the tests under -tags=integration on every IaC-touching PR. Per cycle 1 I-2 + cycle 2 I-1-NEW, `go build` alone leaves wire incompat between workflow and plugin grpc-go versions undetected; this job catches that bug class. - Extends the path filters to gate on plugin/external/**, so changes to the typed sdk helpers + iac.proto trigger this workflow rather than only the AWS/GCP/Azure compile-compat job. - The subprocess wire-test variant against the real DO plugin v1.0.0 binary is added once that plugin ships (per plan §PR 3 / Task 7+). Stacked on feat/iac-sdk-contracts-task5 (Task 5 PR #602 provides BuildContractRegistry; the E2E test exercises the surface from Tasks 3–5 + 29 end-to-end through gRPC). Verification: - GOWORK=off go test -tags=integration -race \ ./plugin/external/sdk/... -run TestIaC_EndToEnd → PASS (2/2) - GOWORK=off go test ./plugin/external/sdk/... → PASS (no regression in non-integration tests) - GOWORK=off go vet -tags=integration ./plugin/external/... → clean - actionlint .github/workflows/cross-plugin-build-test.yml → clean - python yaml.safe_load(...) → parses Rollback: revert this commit; no production code or contract is affected (test + CI YAML only). * test(sdk): pin OptionalNotRegistered E2E test to codes.Unimplemented Per cycle 4 code-review PR 603 MINOR-1: the previous assertion in TestIaC_EndToEnd_OptionalNotRegistered_ClientFailsTyped only checked err != nil — any error (network flake, deadline, transport-layer behavior change) would satisfy it, masking real Unimplemented-vs- other regressions in the absence-of-registration signal. Tightens the assertion to status.Code(err) == codes.Unimplemented so the test specifically pins the design's "absence of registration IS the negative signal" contract end-to-end at the gRPC layer. Verification: GOWORK=off go test -tags=integration -race ./plugin/external/sdk/... -run TestIaC_EndToEnd → PASS (2/2); gofmt clean. * test(sdk): bufconn cleanup + RPC deadline + clarified path filter (PR 603) Per cycle 4 code-review PR 603 (Copilot 4 Important + 1 MINOR): IMPORTANT-2/3 — bufconn listener leak: Both TestIaC_EndToEnd_* tests called bufconn.Listen but never closed the listener. server.Stop in t.Cleanup tears down the *grpc.Server but leaves the listener's accept goroutine alive until -race's GC pressure trips it. Adds `t.Cleanup(func() { _ = listener.Close() })` after each bufconn.Listen call. IMPORTANT-4/5 — RPC deadline: RPCs used context.Background() with no deadline → CI worker hangs until suite-wide timeout on transport failure. Replaces with `ctx, cancel := context.WithTimeout(context.Background(), e2eRPCDeadline)` (5s) + t.Cleanup(cancel). Both tests now bound their RPC time even if the gRPC layer wedges. e2eRPCDeadline lives at package scope alongside e2eBufSize so the per-test allocation reads cleanly and a future timeout bump is one line. MINOR-6 — path-filter intent comment: cross-plugin-build-test.yml `plugin/external/**` filter is broad on purpose — typed contract + sdk helpers + downstream IaC dispatch + remote-plugin orchestration code all live under this dir, and ALL of them affect the iac-typed-e2e job. Comment rewrites to document the intent (was: comment said only sdk helpers, suggesting a narrower path). Verification: GOWORK=off go test -tags=integration -race ./plugin/external/sdk/... -run TestIaC_EndToEnd → PASS (2/2); gofmt clean; actionlint clean. Rollback: revert this commit; bufconn listener leaks return + RPC unbounded; cross-plugin-build path filter intent comment returns to misleading wording.
intel352
added a commit
that referenced
this pull request
May 10, 2026
…d-braces guard (Task 15) (#606) * feat(proto): add iac.proto with IaCProviderRequired + 6 optional services + ResourceDriver Task 3 of the strict-contracts force-cutover plan (docs/plans/2026-05-10-strict-contracts-force-cutover.md, rev5). Adds plugin/external/proto/iac.proto defining the typed gRPC contract that supersedes the legacy InvokeService/structpb dispatch path for IaCProvider + ResourceDriver: - service IaCProviderRequired: 11 RPCs every IaC plugin MUST implement (Initialize, Name, Version, Capabilities, Plan, Apply, Destroy, Status, Import, ResolveSizing, BootstrapStateBackend). Compile-time enforced via the SDK type-assert in Task 4. - 6 optional services — providers register only the ones they support: IaCProviderEnumerator (EnumerateAll, EnumerateByTag), IaCProviderDriftDetector (DetectDrift, DetectDriftWithSpecs), IaCProviderCredentialRevoker (RevokeProviderCredential), IaCProviderMigrationRepairer (RepairDirtyMigration), IaCProviderValidator (ValidatePlan), IaCProviderDriftConfigDetector (DetectDriftConfig). Absence of registration IS the negative signal — no NotSupported field on any optional response (per design §Optional services). - service ResourceDriver: 9 RPCs for per-resource-type CRUD dispatch (Create, Read, Update, Delete, Diff, Scale, HealthCheck, SensitiveKeys, Troubleshoot), each carrying resource_type so a single server can route to the per-type driver implementation. Hard invariants honored: - NO google.protobuf.Struct, NO google.protobuf.Any anywhere. - Free-form per-resource Config/Outputs payloads cross the wire as bytes <name>_json (the plugin owns json.Marshal/Unmarshal); this eliminates the structpb conversion surface that previously dropped map[string]bool entries silently (T3.9 finding). - ResourceOutput.sensitive uses typed map<string, bool> per design. Generated iac.pb.go + iac_grpc.pb.go via protoc v34.1 + protoc-gen-go v1.36.11 + protoc-gen-go-grpc v1.6.1. Failing test (plugin/external/proto/iac_proto_test.go) asserts the generated server interfaces exist and have the methods the design requires — drops in iac.proto cause the test file to fail to compile. Verification: GOWORK=off go test ./plugin/external/proto/... PASSES; GOWORK=off go build ./plugin/... ./cmd/... ./module/... clean. Rollback: revert this commit; legacy InvokeService dispatch in plugin.proto remains functional; the additive-only nature of this PR means no consumer is affected until subsequent tasks wire callers. * feat(sdk): RegisterAllIaCProviderServices auto-registration helper Task 4 of the strict-contracts force-cutover plan (docs/plans/2026-05-10-strict-contracts-force-cutover.md, rev5). Adds plugin/external/sdk/iacserver.go: a single helper that uses Go type-assertion to register every typed IaC gRPC service the provider satisfies, in one call. REQUIRED service: pb.IaCProviderRequiredServer — surfaced as a clear startup-time error if the provider type doesn't satisfy it (rather than failing at the first RPC dispatch with a generic "unimplemented" status). OPTIONAL services (auto-detected): IaCProviderEnumerator, IaCProviderDriftDetector, IaCProviderCredentialRevoker, IaCProviderMigrationRepairer, IaCProviderValidator, IaCProviderDriftConfigDetector. Plus ResourceDriver. Per cycle 3 I-1 of the design: plugin authors write ONE call; they cannot omit registration for a capability they implemented. This removes the registration-omission bug class (the same shape as the legacy InvokeService case-string-typo bug) by removing the manual step entirely. Tests cover four cases: - required-satisfied → required service registered + advertised by grpcSrv.GetServiceInfo(). - enumerator-only → only the optional Enumerator service registered; other optionals stay absent (auto-detection precision). - empty-stub → returns an error naming the unsatisfied required interface, with a docs pointer. - all-capabilities-stub → all 8 typed services (Required + 6 optional + ResourceDriver) registered. Stacked on feat/iac-proto-task3 (Task 3 PR #598 provides the generated server interfaces this helper consumes). Verification: GOWORK=off go test -race ./plugin/external/sdk/... PASS; GOWORK=off go build ./plugin/... ./cmd/... ./module/... clean; GOWORK=off go vet ./plugin/external/... clean. Rollback: revert this commit; SDK consumers can still register services manually via the per-service Register* helpers protoc generated. * feat(sdk): ServeIaCPlugin high-level entrypoint with go-plugin GRPCServer callback Task 29 of the strict-contracts force-cutover plan (docs/plans/2026-05-10-strict-contracts-force-cutover.md, rev5). Adds the high-level plugin-author API on top of Task 4's RegisterAllIaCProviderServices: func main() { sdk.ServeIaCPlugin(&doProvider{}, sdk.IaCServeOptions{}) } Per cycle 3 I-1 of the design, service registration happens INSIDE go-plugin's GRPCServer callback (iacGRPCPlugin.GRPCServer) — the framework owns *grpc.Server lifecycle, so plugin authors cannot pre-create a server and forget to register a typed service on it. API surface (all in plugin/external/sdk/iacserver.go): - IaCServeOptions{ PluginInfo *PluginInfo } — caller-side options. - PluginInfo{ HandshakeConfig goplugin.HandshakeConfig } — extension point for future Name/Version metadata; defaults to ext.Handshake (the canonical wfctl<->plugin handshake) when zero-valued. - iacGRPCPlugin{provider any} — implements goplugin.Plugin (GRPCServer + GRPCClient). The GoCodeAlone fork of go-plugin v1.7.0 is gRPC-only and exposes only the canonical Plugin interface; there is no GRPCPlugin alias or NetRPCUnsupportedPlugin embed to use. - ServeIaCPlugin(provider, opts) — wraps goplugin.Serve with the resolved handshake + a single iacGRPCPlugin entry under the "iac" key. - resolveServeHandshake(opts) — extracted helper so the override-vs- default rule is unit-testable without invoking the blocking goplugin.Serve loop. Tests (iacserver_serve_test.go) cover six cases via internal-package tests (so the unexported plugin type is exercisable without a real subprocess; subprocess-level coverage lands in Task 6's typed-IaC E2E test): - iacGRPCPlugin.GRPCServer registers all satisfied services on the framework-managed *grpc.Server (Required + Enumerator + ResourceDriver for the all-stub). - iacGRPCPlugin.GRPCServer propagates the auto-register error for an empty stub — go-plugin aborts plugin startup with an actionable message. - iacGRPCPlugin.GRPCClient is a no-op (host builds typed clients directly). - iacGRPCPlugin satisfies goplugin.Plugin at compile time (refactor guard). - ServeIaCPlugin defaults to ext.Handshake when PluginInfo is nil. - ServeIaCPlugin honors a non-zero override handshake when provided. Stacked on feat/iac-sdk-auto-register-task4 (Task 4 PR #599 provides RegisterAllIaCProviderServices, which the GRPCServer callback delegates to). Verification: GOWORK=off go test -race ./plugin/external/sdk/... PASS; GOWORK=off go build ./plugin/... ./cmd/... ./module/... clean; GOWORK=off go vet ./plugin/external/... clean. Rollback: revert this commit; plugin authors can fall back to manually constructing goplugin.Serve + Plugins map referencing RegisterAllIaCProviderServices in their own GRPCServer callback. * feat(sdk): BuildContractRegistry advertises registered IaC services Task 5 of the strict-contracts force-cutover plan (docs/plans/2026-05-10-strict-contracts-force-cutover.md, rev5). Adds plugin/external/sdk/contracts.go with the BuildContractRegistry helper that enumerates grpc.Server.GetServiceInfo() and emits a SERVICE-kind ContractDescriptor for each registered service. ContractMode is set to STRICT_PROTO so the host can distinguish typed IaC services from the legacy structpb-mode contracts produced by Module/Step/Trigger ContractProvider implementations. Per cycle 3 I-1 of the design: wfctl needs a single mechanism to discover "is the optional service registered on this plugin handle?". Reusing the existing ContractRegistry shape keeps Module/Step/Trigger and IaC capability discovery on the same wire surface — no new gRPC server-reflection dependency required. Service descriptors are emitted in deterministic alphabetical order so callers can rely on stable output for diff/compare operations and the wftest BDD test in Task 15. The helper is safe to call with a nil server (returns an empty but non-nil ContractRegistry) so callers that may construct it before the gRPC server exists do not panic. Tests (contracts_iac_test.go) cover three cases — all pass: - AdvertisesRegisteredIaCServices: a Required + Enumerator + DriftDetector stub yields exactly those service descriptors. - ServiceContractsUseStrictProtoMode: every emitted descriptor is Kind=SERVICE + Mode=STRICT_PROTO (host-side discriminator). - NilServer_ReturnsEmpty: defensive contract for nil input. Stacked on feat/iac-sdk-serve-task29 (Task 29 PR #600 provides ServeIaCPlugin which IaC plugins use to register the services this helper enumerates). Verification: GOWORK=off go test -race ./plugin/external/sdk/... PASS; GOWORK=off go build ./plugin/... ./cmd/... ./module/... clean; GOWORK=off go vet ./plugin/external/... clean. Rollback: revert this commit; ContractRegistry returns the prior shape (Module/Step/Trigger only via the existing ContractProvider hook in grpc_server.go). * test(wftest/bdd): AssertProviderCapabilitiesMatchRegistration belt-and-braces guard Task 15 of the strict-contracts force-cutover plan (docs/plans/2026-05-10-strict-contracts-force-cutover.md, rev5). Adds wftest/bdd/strict_iac.go with the AssertProviderCapabilitiesMatchRegistration helper. Given a Go provider implementation + the gRPC server with the plugin's service registrations, the helper asserts: - Pass 1: every typed IaC service interface satisfied by the provider's Go type IS registered on the gRPC server. - Pass 2: no IaC service is registered on the gRPC server that the provider's Go type does NOT satisfy (catches the "manual Register* call binds a different Go impl" failure mode). Per cycle 4 belt-and-braces of the design: the canonical registration path is sdk.RegisterAllIaCProviderServices, which uses Go type-assertion to auto-detect every interface and cannot omit a registration. The per-service Register* helpers are still exposed for advanced use cases; this helper is the test-time guard that catches the manual-registration omission failure mode. iacServiceChecks lists every typed IaC service the helper knows about (Required + 6 optional + ResourceDriver). New optional services added to iac.proto must be appended here. Helper takes a strictIaCT interface (Errorf + Fatalf + Helper) rather than *testing.T directly so the failure path itself is unit-testable via a recordingT double — the four tests in strict_iac_test.go cover the four behavior axes: - AutoRegisteredAllOK: silent pass when sdk.RegisterAllIaCProviderServices was used as designed. - ManuallyRegisteredMissingOptional_Fails: provider satisfies every optional + ResourceDriver but author registered only Required + Enumerator manually → helper names every missing service in the error messages. - ProviderMissingRequired_Fails: broken test fixture (provider doesn't satisfy IaCProviderRequiredServer) → fatal-class failure with the missing interface named. - RegisteredButProviderDoesntSatisfy_Fails: server has Enumerator registered but provider passed to assert is requiredOnlyStub → over-registration named in error. Stacked on feat/iac-sdk-contracts-task5 (Task 5 PR #602 provides the typed proto interfaces this helper inspects via reflection- free type-assert chain). Verification: GOWORK=off go test -race ./wftest/... PASS; GOWORK=off go build ./... clean; GOWORK=off go vet ./wftest/... clean. Rollback: revert this commit; canonical registration path (sdk.RegisterAllIaCProviderServices) is unaffected. * test(wftest/bdd): protoreflect coverage check for iacServiceChecks Per cycle 4 code-review PR 606 MINOR-1: iacServiceChecks in wftest/bdd/strict_iac.go is a manually-maintained list of typed IaC services. If iac.proto adds a new optional service later without appending to iacServiceChecks, AssertProviderCapabilitiesMatchRegistration silently passes for the new service (the satisfies check is never invoked for unlisted services), leaving a hole in the cycle 4 belt- and-braces invariant. Adds wftest/bdd/strict_iac_internal_test.go (package bdd, internal test so iacServiceChecks is in scope) with TestIaCServiceChecks_CoversEveryProtoService — walks pb.File_plugin_external_proto_iac_proto.Services() at runtime and asserts: - Forward: every IaC service name in the proto (prefix-matched on iacServicePrefix) appears in iacServiceChecks. Missing entries produce a test failure naming the service(s) the proto declares but the helper doesn't cover. - Inverse: every iacServiceCheck row names a service the proto actually declares. Catches the rename-without-cleanup failure mode (proto renames a service; iacServiceChecks still references the old name). Tightens the manual-maintenance surface into a compile-time- discoverable test failure rather than a silent test-passing regression. Verification: GOWORK=off go test ./wftest/bdd/... -run "TestIaCServiceChecks|TestAssertProviderCapabilities" -count=1 → PASS (5/5); gofmt clean. Rollback: revert this commit; the cycle 4 belt-and-braces guard returns to the manual-maintenance form (still works, just no test-time coverage check on the helper itself). * fix(wftest/bdd): use generated File_iac_proto descriptor (post-cascade-merge) Generated proto descriptor variable is File_iac_proto (per iac.proto's go_package option), not File_plugin_external_proto_iac_proto. Reference fixed; all 5 BDD strict-IaC tests PASS. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
intel352
added a commit
that referenced
this pull request
May 10, 2026
…tover runbook (Task 18) (#610) * feat(proto): add iac.proto with IaCProviderRequired + 6 optional services + ResourceDriver Task 3 of the strict-contracts force-cutover plan (docs/plans/2026-05-10-strict-contracts-force-cutover.md, rev5). Adds plugin/external/proto/iac.proto defining the typed gRPC contract that supersedes the legacy InvokeService/structpb dispatch path for IaCProvider + ResourceDriver: - service IaCProviderRequired: 11 RPCs every IaC plugin MUST implement (Initialize, Name, Version, Capabilities, Plan, Apply, Destroy, Status, Import, ResolveSizing, BootstrapStateBackend). Compile-time enforced via the SDK type-assert in Task 4. - 6 optional services — providers register only the ones they support: IaCProviderEnumerator (EnumerateAll, EnumerateByTag), IaCProviderDriftDetector (DetectDrift, DetectDriftWithSpecs), IaCProviderCredentialRevoker (RevokeProviderCredential), IaCProviderMigrationRepairer (RepairDirtyMigration), IaCProviderValidator (ValidatePlan), IaCProviderDriftConfigDetector (DetectDriftConfig). Absence of registration IS the negative signal — no NotSupported field on any optional response (per design §Optional services). - service ResourceDriver: 9 RPCs for per-resource-type CRUD dispatch (Create, Read, Update, Delete, Diff, Scale, HealthCheck, SensitiveKeys, Troubleshoot), each carrying resource_type so a single server can route to the per-type driver implementation. Hard invariants honored: - NO google.protobuf.Struct, NO google.protobuf.Any anywhere. - Free-form per-resource Config/Outputs payloads cross the wire as bytes <name>_json (the plugin owns json.Marshal/Unmarshal); this eliminates the structpb conversion surface that previously dropped map[string]bool entries silently (T3.9 finding). - ResourceOutput.sensitive uses typed map<string, bool> per design. Generated iac.pb.go + iac_grpc.pb.go via protoc v34.1 + protoc-gen-go v1.36.11 + protoc-gen-go-grpc v1.6.1. Failing test (plugin/external/proto/iac_proto_test.go) asserts the generated server interfaces exist and have the methods the design requires — drops in iac.proto cause the test file to fail to compile. Verification: GOWORK=off go test ./plugin/external/proto/... PASSES; GOWORK=off go build ./plugin/... ./cmd/... ./module/... clean. Rollback: revert this commit; legacy InvokeService dispatch in plugin.proto remains functional; the additive-only nature of this PR means no consumer is affected until subsequent tasks wire callers. * feat(sdk): RegisterAllIaCProviderServices auto-registration helper Task 4 of the strict-contracts force-cutover plan (docs/plans/2026-05-10-strict-contracts-force-cutover.md, rev5). Adds plugin/external/sdk/iacserver.go: a single helper that uses Go type-assertion to register every typed IaC gRPC service the provider satisfies, in one call. REQUIRED service: pb.IaCProviderRequiredServer — surfaced as a clear startup-time error if the provider type doesn't satisfy it (rather than failing at the first RPC dispatch with a generic "unimplemented" status). OPTIONAL services (auto-detected): IaCProviderEnumerator, IaCProviderDriftDetector, IaCProviderCredentialRevoker, IaCProviderMigrationRepairer, IaCProviderValidator, IaCProviderDriftConfigDetector. Plus ResourceDriver. Per cycle 3 I-1 of the design: plugin authors write ONE call; they cannot omit registration for a capability they implemented. This removes the registration-omission bug class (the same shape as the legacy InvokeService case-string-typo bug) by removing the manual step entirely. Tests cover four cases: - required-satisfied → required service registered + advertised by grpcSrv.GetServiceInfo(). - enumerator-only → only the optional Enumerator service registered; other optionals stay absent (auto-detection precision). - empty-stub → returns an error naming the unsatisfied required interface, with a docs pointer. - all-capabilities-stub → all 8 typed services (Required + 6 optional + ResourceDriver) registered. Stacked on feat/iac-proto-task3 (Task 3 PR #598 provides the generated server interfaces this helper consumes). Verification: GOWORK=off go test -race ./plugin/external/sdk/... PASS; GOWORK=off go build ./plugin/... ./cmd/... ./module/... clean; GOWORK=off go vet ./plugin/external/... clean. Rollback: revert this commit; SDK consumers can still register services manually via the per-service Register* helpers protoc generated. * feat(sdk): ServeIaCPlugin high-level entrypoint with go-plugin GRPCServer callback Task 29 of the strict-contracts force-cutover plan (docs/plans/2026-05-10-strict-contracts-force-cutover.md, rev5). Adds the high-level plugin-author API on top of Task 4's RegisterAllIaCProviderServices: func main() { sdk.ServeIaCPlugin(&doProvider{}, sdk.IaCServeOptions{}) } Per cycle 3 I-1 of the design, service registration happens INSIDE go-plugin's GRPCServer callback (iacGRPCPlugin.GRPCServer) — the framework owns *grpc.Server lifecycle, so plugin authors cannot pre-create a server and forget to register a typed service on it. API surface (all in plugin/external/sdk/iacserver.go): - IaCServeOptions{ PluginInfo *PluginInfo } — caller-side options. - PluginInfo{ HandshakeConfig goplugin.HandshakeConfig } — extension point for future Name/Version metadata; defaults to ext.Handshake (the canonical wfctl<->plugin handshake) when zero-valued. - iacGRPCPlugin{provider any} — implements goplugin.Plugin (GRPCServer + GRPCClient). The GoCodeAlone fork of go-plugin v1.7.0 is gRPC-only and exposes only the canonical Plugin interface; there is no GRPCPlugin alias or NetRPCUnsupportedPlugin embed to use. - ServeIaCPlugin(provider, opts) — wraps goplugin.Serve with the resolved handshake + a single iacGRPCPlugin entry under the "iac" key. - resolveServeHandshake(opts) — extracted helper so the override-vs- default rule is unit-testable without invoking the blocking goplugin.Serve loop. Tests (iacserver_serve_test.go) cover six cases via internal-package tests (so the unexported plugin type is exercisable without a real subprocess; subprocess-level coverage lands in Task 6's typed-IaC E2E test): - iacGRPCPlugin.GRPCServer registers all satisfied services on the framework-managed *grpc.Server (Required + Enumerator + ResourceDriver for the all-stub). - iacGRPCPlugin.GRPCServer propagates the auto-register error for an empty stub — go-plugin aborts plugin startup with an actionable message. - iacGRPCPlugin.GRPCClient is a no-op (host builds typed clients directly). - iacGRPCPlugin satisfies goplugin.Plugin at compile time (refactor guard). - ServeIaCPlugin defaults to ext.Handshake when PluginInfo is nil. - ServeIaCPlugin honors a non-zero override handshake when provided. Stacked on feat/iac-sdk-auto-register-task4 (Task 4 PR #599 provides RegisterAllIaCProviderServices, which the GRPCServer callback delegates to). Verification: GOWORK=off go test -race ./plugin/external/sdk/... PASS; GOWORK=off go build ./plugin/... ./cmd/... ./module/... clean; GOWORK=off go vet ./plugin/external/... clean. Rollback: revert this commit; plugin authors can fall back to manually constructing goplugin.Serve + Plugins map referencing RegisterAllIaCProviderServices in their own GRPCServer callback. * feat(sdk): BuildContractRegistry advertises registered IaC services Task 5 of the strict-contracts force-cutover plan (docs/plans/2026-05-10-strict-contracts-force-cutover.md, rev5). Adds plugin/external/sdk/contracts.go with the BuildContractRegistry helper that enumerates grpc.Server.GetServiceInfo() and emits a SERVICE-kind ContractDescriptor for each registered service. ContractMode is set to STRICT_PROTO so the host can distinguish typed IaC services from the legacy structpb-mode contracts produced by Module/Step/Trigger ContractProvider implementations. Per cycle 3 I-1 of the design: wfctl needs a single mechanism to discover "is the optional service registered on this plugin handle?". Reusing the existing ContractRegistry shape keeps Module/Step/Trigger and IaC capability discovery on the same wire surface — no new gRPC server-reflection dependency required. Service descriptors are emitted in deterministic alphabetical order so callers can rely on stable output for diff/compare operations and the wftest BDD test in Task 15. The helper is safe to call with a nil server (returns an empty but non-nil ContractRegistry) so callers that may construct it before the gRPC server exists do not panic. Tests (contracts_iac_test.go) cover three cases — all pass: - AdvertisesRegisteredIaCServices: a Required + Enumerator + DriftDetector stub yields exactly those service descriptors. - ServiceContractsUseStrictProtoMode: every emitted descriptor is Kind=SERVICE + Mode=STRICT_PROTO (host-side discriminator). - NilServer_ReturnsEmpty: defensive contract for nil input. Stacked on feat/iac-sdk-serve-task29 (Task 29 PR #600 provides ServeIaCPlugin which IaC plugins use to register the services this helper enumerates). Verification: GOWORK=off go test -race ./plugin/external/sdk/... PASS; GOWORK=off go build ./plugin/... ./cmd/... ./module/... clean; GOWORK=off go vet ./plugin/external/... clean. Rollback: revert this commit; ContractRegistry returns the prior shape (Module/Step/Trigger only via the existing ContractProvider hook in grpc_server.go). * feat(wfctl): IaC plugin pre-flight gate building block + iac-typed-cutover runbook Task 18 of the strict-contracts force-cutover plan (docs/plans/2026-05-10-strict-contracts-force-cutover.md, rev5). Adds two pieces: 1. cmd/wfctl/iac_loader_gate.go — the pre-flight gate building block. AssertIaCPluginAdvertisesRequiredService(name, version, registry) inspects a *pb.ContractRegistry response from GetContractRegistry and returns nil iff the plugin advertises workflow.plugin.external.iac.IaCProviderRequired as a CONTRACT_KIND_SERVICE descriptor. On failure: an actionable error naming the offending plugin + version, citing .wfctl-lock.yaml as the migration target, and pointing to docs/runbooks/iac-typed-cutover.md, wrapping errLegacyIaCPlugin for IsLegacyIaCPluginErr dispatch. The function is the load-bearing predicate the deploy / infra-plan / infra-apply call sites will invoke after their GetContractRegistry RPC succeeds (wiring left for the typed- client cutover already in progress in Task 16; this commit ships the testable predicate so the wiring is one-line drop-in). 2. docs/runbooks/iac-typed-cutover.md — operator-facing runbook for the cutover. Covers: - Compatibility matrix (wfctl × DO plugin version combinations, pre-flight expected outcomes). - Upgrade order: plugins first, then wfctl. Documents WHY this ordering matters (legacy wfctl + v1.0.0 plugin works; v1.0.0 wfctl + legacy plugin refuses). - .wfctl-lock.yaml migration (lockfile shape unchanged; only the version pin needs editing). - Troubleshooting: legacy-plugin pre-flight error, EnumeratorAll-style messages from pre-cutover wfctl, state-file decode regressions, advertised-but-buggy plugins. - Backout plan: re-pin legacy plugin + wfctl while in the rc1 window. Tests (cmd/wfctl/iac_loader_gate_test.go) — 7 cases, all PASS: - TypedRegistryAccepts: SERVICE-kind descriptor for IaCProviderRequired. - LegacyRegistryRejects: ContractRegistry without the required service → wrapped error names plugin + version + runbook. - NilRegistryRejects: defensive contract for nil registry input. - EmptyContractsRejects: no descriptors at all → still legacy. - WrongKindRejects: ServiceName matches but Kind != SERVICE. - EmptyMetadataDefaults: name/version absent → graceful "<unknown>". - IsLegacyIaCPluginErr_NoFalsePositives: sentinel match is typed (not string-based) so dispatch sites can errors.Is cleanly. Stacked on feat/iac-sdk-contracts-task5 (Task 5 PR #602 provides BuildContractRegistry — the wfctl-side gate inspects what plugins emit via that helper, so this PR builds on it). Verification: GOWORK=off go test -race ./cmd/wfctl/... PASS; GOWORK=off go vet ./cmd/wfctl/ clean; gofmt clean. Rollback: revert this commit. The pre-flight gate is a standalone predicate function — no production code currently depends on it (dispatch wiring lands in the typed-client cutover Task 16). The runbook is documentation; no operational impact from removal. * docs(runbook): correct cutover model — wfctl-side rc1 adapter, not plugin compat shim Per spec-reviewer's PR 610 IMPORTANT-1 + team-lead's ruling on the correct cutover model (which I as ADR 0024 author should have known already): The runbook previously claimed "v1.0.0 plugins also expose the legacy InvokeService surface during the rc1 window for backward compatibility" and "backout fully supported through rc1 window (workflow v1.0.0-rc1 + DO plugin v1.0.0)". Both wrong. Contradicts ADR 0024 (force-cutover, no compat shim, no build-tag dual-path), Task 9 spec literal ("DELETE module_instance.go"), and workspace memory feedback_force_strict_contracts_no_compat. Corrected cutover model: - workflow v1.0.0-rc1 ships the wfctl-SIDE typed-client adapter alongside the existing wfctl remoteIaCProvider. Plugins are unchanged at v0.14.x; the typed adapter is exercised only when a typed-aware plugin is loaded. This is rc1's role: wfctl-side additive, not plugin-side compat shim. - DO plugin v1.0.0 ships typed-only (Task 9 deletes the legacy internal/module_instance.go switch dispatcher entirely; ResourceDriver Task 11 follows the same pattern). - workflow v1.0.0 final ships typed-only on the wfctl side too (Task 20 removes wfctl-side remoteIaCProvider). Both legacy paths are gone. Specific revisions applied: 1. Compatibility matrix: added the missing v0.27.x × v1.0.0 row marked "BROKEN by design" — operators MUST run wfctl rc1+ to consume DO v1.0.0 because the legacy wfctl binary cannot dispatch through a typed-only plugin. Added explicit Step 4 troubleshooting block for operators who skipped wfctl rc1. 2. Upgrade order rewrite: 5 steps (was 3). Order is wfctl rc1 first (test against v0.14.x plugin set), then DO plugin v1.0.0, then wfctl v1.0.0 final. Each step has its smoke-test command and rollback escape. 3. Backout plan rewrite: explicit "rollback BOTH wfctl AND plugin together" for the post-v1.0.0-final case. Single-side rollback is broken — wfctl v1.0.0 final binary cannot dispatch v0.14.x plugin (legacy adapter deleted in Task 20); legacy v0.27.x wfctl cannot dispatch typed-only DO v1.0.0 (no typed adapter on the legacy binary). Documents the partial-rollback option that exists during the rc1 window only. 4. Top-of-runbook callout: explicit statement that this is a hard cutover with no plugin-side compat shim, citing ADR 0024 and feedback_force_strict_contracts_no_compat. No code changes; documentation only. Verification: markdown rendered locally; cross-references to ADRs + plan + design + state_compat_test.go all resolve. Followups (per spec-reviewer + team-lead Finding 2): - PR description gets a "Wiring deferred to Task 16 (PR 609)" callout with predicate symbol + sentinel + call-site location - Coordination DM to implementer-2 with the wiring contract * refactor(wfctl): rename iacRequiredServiceName → iacServiceRequired Cross-task naming coordination with PR #605/#609 (implementer-2, Tasks 30/16). Their iac_typed_adapter.go declares 8 sibling consts naming every typed IaC service: iacServiceRequired iacServiceEnumerator iacServiceDriftDetector iacServiceCredentialRevoker iacServiceMigrationRepairer iacServiceValidator iacServiceDriftConfigDetect iacServiceResourceDriver PR #610 originally introduced its own const iacRequiredServiceName for the same Required service FQN. Spec-reviewer flagged the inconsistency and asked us to coordinate. Implementer-2 + I agreed their convention wins (8 siblings establish the pattern; my single const matches). Pure rename; no behavior change. Tests still pass. Verification: GOWORK=off go test ./cmd/wfctl/ -run \ "TestAssertIaCPluginAdvertises|TestIsLegacyIaCPluginErr" \ -count=1 → PASS (7/7); gofmt clean. Once PR #610 merges, implementer-2's PR #609 rebase can drop their duplicate const and import iacServiceRequired from here. * fix(wfctl): drop duplicate iacServiceRequired const (now lives in iac_typed_adapter.go on main) PR #605 merged iac_typed_adapter.go to main with iacServiceRequired const. PR #610's iac_loader_gate.go declared it independently; collision after cascade-merge. Removed the duplicate; use canonical const from iac_typed_adapter.go directly. Build clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Task 3 of the strict-contracts force-cutover plan (docs/plans/2026-05-10-strict-contracts-force-cutover.md, rev5, scope-locked at
e82b7e0c).Adds the typed gRPC contract that supersedes the legacy
InvokeService+structpbdispatch path forIaCProvider+ResourceDriver. This PR is additive only — no consumer is affected; subsequent tasks wire the callers.What's added
plugin/external/proto/iac.proto— the typed contract:service IaCProviderRequired(11 RPCs every IaC plugin MUST implement; compile-time enforced via SDK type-assert in Task 4)service ResourceDriver(9 RPCs for per-resource-type CRUD dispatch withresource_typeon every request)plugin/external/proto/iac.pb.go+iac_grpc.pb.go— protoc-generated Go (protoc v34.1 + protoc-gen-go v1.36.11 + protoc-gen-go-grpc v1.6.1)plugin/external/proto/iac_proto_test.go— failing-first conformance test that the generated server interfaces exist with the methods the design requires (drops in iac.proto cause compile failure)Hard invariants honored
google.protobuf.Struct, NOgoogle.protobuf.Anyanywhere in the protoConfig/Outputspayloads cross the wire asbytes <name>_json(plugin ownsjson.Marshal/Unmarshal); eliminates the structpb conversion surface that silently droppedmap[string]boolentries (T3.9 finding)ResourceOutput.sensitiveuses typedmap<string, bool>per designVerification
GOWORK=off go test ./plugin/external/proto/... -count=1→ PASS (race-mode also clean)GOWORK=off go build ./plugin/... ./cmd/... ./module/...→ cleanRollback
Revert this commit; legacy
InvokeServicedispatch inplugin.protoremains functional; additive-only.Test plan
GOWORK=off go test ./plugin/external/proto/...passesGOWORK=off go test -race ./plugin/external/proto/...passesGOWORK=off go build ./plugin/... ./cmd/... ./module/...cleaniac.pb.go+iac_grpc.pb.gofromiac.protovia tooling pinned in Task 2'stools.gogoogle.protobuf.Struct/google.protobuf.Anyreferences iniac.proto(only in superseded comment)🤖 Generated with Claude Code