feat(curtailment): add proto/SDK contracts and capability flags#118
feat(curtailment): add proto/SDK contracts and capability flags#118rongxin-liu merged 31 commits intomainfrom
Conversation
Foundational scaffolding for curtailment v1 (#116). Defines the public API surface and SDK/plugin contract that every later piece depends on, and wires empty (Unimplemented) RPC handlers so the proto types and route registrations exist before persistence, selection, dispatch, reconciliation, and restore land in follow-ups. - New proto/curtailment/v1/curtailment.proto with CurtailmentService, mode/ strategy/level/priority/state enums (with explicit reservations for v2/v3/v4 values), oneof scope/mode_params requests, FixedKw/FixedPercent params, the include_maintenance / force_include_maintenance safety pair, and cursor-paginated ListCurtailmentEvents. - proto/capabilities/v1/capabilities.proto: CommandCapabilities gains curtail_supported (v1) plus reserved curtail_efficiency_supported and curtail_partial_supported (v4). - server/sdk/v1: new CurtailLevel enum and Curtail/Uncurtail methods on DeviceControl, ErrCurtailCapabilityNotSupported / ErrCurtailTransient sentinels, gRPC server+client bridge wiring, and matching capability string constants for plugin Capabilities maps. - server/sdk/v1/pb/driver.proto: Curtail/Uncurtail RPCs and CurtailLevel message so plugins implement curtailment over the existing gRPC bridge. - server/internal/domain/miner/interfaces.Miner: Curtail/Uncurtail; PluginMiner adapter wraps sdk.Device via wrapPluginError. Mocks regenerated. - server/internal/domain/plugins/capabilities.go maps the new SDK capability strings into the proto CommandCapabilities flags. - server/internal/handlers/curtailment: empty handler returns CodeUnimplemented for every v1 RPC; registered in cmd/fleetd/main.go next to the existing Connect handlers. Test asserts every RPC is reachable and returns Unimplemented. - Plugin implementations: - virtual: full Curtail(FULL)/Uncurtail with state tracking and tests covering FULL, unsupported levels (efficiency/partial → permanent capability error), and idempotent uncurtail. - proto / antminer: wrap StopMining/StartMining for FULL; unsupported levels return ErrCurtailCapabilityNotSupported. - asicrs (Rust): wraps stop_mining/start_mining for Full level via tonic-generated trait; unsupported levels return Unimplemented. Acceptance: - buf lint and buf generate produce clean Go/TypeScript/Python outputs. - Existing command and plugin capability behavior is unchanged for non-curtailment commands. - Virtual plugin reports curtail_supported and executes Curtail(FULL) / Uncurtail against the device test fixture. - All v1 RPC stubs are reachable via the Connect router but return Unimplemented. Made-with: Cursor
🔐 Codex Security Review
Review SummaryOverall Risk: MEDIUM Findings[HIGH] Proto “efficiency” curtailment can raise wattage instead of reducing it
[MEDIUM] Uncurtail defaults to “start mining” when its snapshot is missing
[MEDIUM] ASIC-RS now advertises full-curtail support before it has been proven
Notes
Generated by Codex Security Review | |
Two staleness checks failed against the foundation commit: - cargo fmt rejected the asicrs Curtail::Unspecified match arm because the single-line Err wrapped its argument; rustfmt prefers a block-form arm there. Reformatted accordingly. - driver_pb2_grpc.py was committed against grpcio-tools 1.76.0, but packages/proto-python-gen/requirements.txt pins 1.80.0 and CI generates with that version. Regenerated the stub against the pinned toolchain so GRPC_GENERATED_VERSION matches what CI produces. Made-with: Cursor
ec4e78a to
56ac56c
Compare
…emantics Address review feedback on PR #118: - Add buf.validate CEL rules to PreviewCurtailmentPlanRequest and StartCurtailmentRequest requiring exactly one scope and pairing mode_params with the chosen mode (FIXED_KW <-> fixed_kw, FIXED_PERCENT <-> fixed_percent). - Drop redundant not_in: [0] mode constraint and document UNSPECIFIED-as-default semantics for strategy and level. - Correct misleading page_size comment on ListCurtailmentEventsRequest. - Remove unnecessary safeIntToInt32 cast on CurtailLevel (already int32). - Align asicrs Curtail behavior with the Go plugins: any non-FULL level (including Unspecified) returns Unimplemented as a permanent capability error. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- asicrs: probe supports_pause && supports_resume to gate CAP_CURTAIL, and advertise the FULL level optimistically at the driver level via PROBED_CAPS. Add unit tests for the AND-gate and the driver-level advertisement. - proto and virtual drivers: advertise CapabilityCurtail at DescribeDriver alongside their existing per-device advertisement. Add a virtual driver_test.go locking in the contract. - Rust and Python SDKs: export CAP_CURTAIL / CAP_CURTAIL_EFFICIENCY / CAP_CURTAIL_PARTIAL constants so plugin authors can reference the capability strings without hardcoding. - Extend the proto plugin's required-caps test to assert CapabilityCurtail is advertised. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DriverGRPCServer methods that returned device-call errors directly (StartMining, StopMining, BlinkLED, Reboot, SetCoolingMode, SetPowerTarget, UpdateMiningPools, UpdateMinerPassword, FirmwareUpdate, Close, Unpair, Curtail, Uncurtail) now route those errors through sdkErrorToGRPCStatus so SDKError codes (Unauthenticated, Unavailable, Unimplemented, etc.) propagate to wrapPluginError as the correct gRPC status instead of being read as Internal. Add a parameterized TestDriverGRPCServer_ControlRPCsMapSDKErrorStatus covering one method per category against three distinct codes (StartMining/Unauthenticated, SetCoolingMode/Unavailable, UpdateMinerPassword/Unimplemented). Drop the redundant errors.Unwrap from the existing Curtail/Uncurtail bridge tests since status.FromError walks the wrap chain via errors.As. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
golangci-lint flagged 'new' as shadowing the predeclared identifier in the updateMinerPasswordFunc type. Use the same parameter names as the underlying interface (currentPassword, newPassword) for clarity. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds the v1 “curtailment foundation” across the public API (proto/Connect), SDK/plugin bridge, and plugin implementations, including capability flags and stub server wiring so downstream work can build against stable contracts.
Changes:
- Introduces
curtailment.v1proto service/types and registers stub Connect handlers returningUnimplementedafter validation. - Extends the SDK driver bridge with
Curtail/Uncurtail, curtailment capability flags, and new SDK error codes with gRPC/Connect error mapping updates. - Updates plugins (virtual, proto, antminer, asicrs) to advertise and/or implement FULL curtailment via stop/start or pause/resume, plus adds targeted tests.
Reviewed changes
Copilot reviewed 42 out of 52 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| server/sdk/v1/python/tests/test_utils.py | Adds Python test ensuring curtailment capability constants are exported. |
| server/sdk/v1/python/proto_fleet_sdk/generated/pb/driver_pb2_grpc.py | Generated Python gRPC stubs updated for Curtail/Uncurtail. |
| server/sdk/v1/python/proto_fleet_sdk/generated/pb/driver_pb2.pyi | Generated Python type stubs updated with curtailment types. |
| server/sdk/v1/python/proto_fleet_sdk/generated/pb/driver_pb2.py | Generated Python protobuf updated with curtailment enum/message/service. |
| server/sdk/v1/python/proto_fleet_sdk/capabilities.py | Exports Python capability constants for curtailment. |
| server/sdk/v1/python/proto_fleet_sdk/init.py | Re-exports new Python capability constants at package root. |
| server/sdk/v1/plugin_test.go | Adds Go tests for SDKError→gRPC mapping, including curtailment and control RPCs. |
| server/sdk/v1/plugin.go | Adds driver bridge Curtail/Uncurtail and centralizes SDKError→gRPC mapping for control RPCs. |
| server/sdk/v1/pb/generated/driver_grpc.pb.go | Generated Go gRPC bindings updated with Curtail/Uncurtail. |
| server/sdk/v1/pb/driver.proto | Adds curtailment level enum, request, and driver RPCs to SDK bridge proto. |
| server/sdk/v1/mocks/mock_driver.go | Generated Go mocks updated to include DeviceCurtailment. |
| server/sdk/v1/mocks/generate.go | Updates mockgen target list to include DeviceCurtailment. |
| server/sdk/v1/interface.go | Adds CurtailLevel, DeviceCurtailment, and curtail capability strings to SDK interfaces. |
| server/sdk/v1/errors.go | Adds curtailment-specific SDK error codes and constructors. |
| server/sdk/v1/README.md | Documents optional DeviceCurtailment interface. |
| server/internal/handlers/interceptors/error_mapping_test.go | Adds tests ensuring Connect errors are preserved and other errors mapped. |
| server/internal/handlers/interceptors/error_mapping.go | Preserves *connect.Error values in the error mapping interceptor. |
| server/internal/handlers/curtailment/handler_test.go | Adds tests for stub curtailment routes and validation behavior. |
| server/internal/handlers/curtailment/handler.go | Adds stub curtailment handler returning Unimplemented for all RPCs. |
| server/internal/domain/plugins/plugin_miner_test.go | Adds tests for PluginMiner curtailment dispatch and error taxonomy. |
| server/internal/domain/plugins/plugin_miner.go | Adds Curtail/Uncurtail dispatch through optional SDK support with specialized error wrapping. |
| server/internal/domain/plugins/plugin_factory.go | Extends new-device error classification switch to recognize curtailment SDK error codes. |
| server/internal/domain/plugins/capabilities.go | Maps SDK curtail flags into proto CommandCapabilities. |
| server/internal/domain/miner/interfaces/mocks/mock_miner.go | Generated miner mocks updated with Curtail/Uncurtail. |
| server/internal/domain/miner/interfaces/miner.go | Extends miner interface with curtailment methods. |
| server/internal/domain/fleeterror/error_test.go | Adds unit tests for IsUnavailableError. |
| server/internal/domain/fleeterror/error.go | Adds NewUnavailableErrorf and IsUnavailableError. |
| server/internal/domain/command/execution_service_credentials_test.go | Updates a test mock miner to satisfy the expanded interface. |
| server/generated/grpc/curtailment/v1/curtailmentv1connect/curtailment.connect.go | Generated Connect bindings for the new curtailment service. |
| server/generated/grpc/capabilities/v1/capabilities.pb.go | Generated Go proto updated with new curtailment capability fields. |
| server/cmd/fleetd/main.go | Registers the new curtailment Connect handler routes in fleetd. |
| sdk/rust/proto-fleet-plugin/src/capabilities.rs | Adds Rust plugin SDK capability constants for curtailment. |
| proto/curtailment/v1/curtailment.proto | Introduces the curtailment v1 public API contract (enums/messages/service + validation). |
| proto/capabilities/v1/capabilities.proto | Adds curtailment capability flags to CommandCapabilities. |
| plugin/virtual/internal/driver/driver_test.go | Tests virtual driver advertises FULL curtail capability. |
| plugin/virtual/internal/driver/driver.go | Virtual driver advertises curtailment capability. |
| plugin/virtual/internal/device/device_test.go | Tests virtual device FULL curtail/uncurtail behavior and reserved level rejection. |
| plugin/virtual/internal/device/device.go | Implements FULL curtail/uncurtail in virtual device and invalidates cached status. |
| plugin/proto/tests/unit/plugin_test.go | Extends proto plugin tests to require curtail capability. |
| plugin/proto/internal/driver/driver.go | Proto driver advertises FULL curtail capability. |
| plugin/proto/internal/device/device_test.go | Adds tests for curtail/uncurtail error wrapping and auth/unsupported behavior. |
| plugin/proto/internal/device/device.go | Implements FULL curtail/uncurtail via stop/start and wraps dispatch failures as transient. |
| plugin/asicrs/src/driver.rs | Adds driver-side curtail/uncurtail RPC handling (FULL-only) with tests. |
| plugin/asicrs/src/device.rs | Implements FULL curtail/uncurtail via pause/resume with capability gating + tests. |
| plugin/asicrs/src/capabilities.rs | Adds curtail caps, probes gating, and tests for pause+resume requirement. |
| plugin/antminer/internal/driver/driver.go | Antminer driver advertises FULL curtail capability. |
| plugin/antminer/internal/device/device_test.go | Adds tests for cache invalidation and transient/unsupported curtailment errors. |
| plugin/antminer/internal/device/device.go | Implements FULL curtail/uncurtail via stop/start and invalidates cached status. |
| client/src/protoFleet/api/generated/capabilities/v1/capabilities_pb.ts | Generated TS updated with new curtailment capability fields. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 517ad5273e
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9d12186185
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
mcharles-square
left a comment
There was a problem hiding this comment.
Generally looks good
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d5335ea0f9
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a7c7e22995
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
…-2 foundation) Lays the foundation for the BE-2 ticket (#140): the curtailment persistence layer, sqlc-backed store, domain models, the FIXED_KW mode implementation, and the enum-stability guard for the AdminTerminateEvent validator pinned in BE-1.x (#173). Migration 000040: - curtailment_event with full lifecycle columns plus the BE-1.x admin-only fields (allow_unbounded BOOLEAN, effective_batch_size INT). CHECK constraints enforce maintenance-pair consistency, non-empty external source/reference/idempotency_key, and non-empty reason. Partial UNIQUE indexes cover idempotency, webhook dedupe, and active-event lookup. - curtailment_target with composite PK (event_id, device_identifier), partial indexes for pending work and active-by-device schedule lookup. - curtailment_reconciler_heartbeat singleton seeded at migration time so the staleness alert always has a row to read. - curtailment_org_config with per-org tunables seeded one row per existing org in the same migration transaction; down migration drops the table. Domain layer: - server/internal/domain/curtailment/models defines the boundary shapes (Event, Target, OrgConfig, Heartbeat, EventState/TargetState typed wrappers) so selector/handler/modes do not import sqlc-generated code. - server/internal/domain/curtailment/modes ships the Mode interface and the FixedKw implementation. Pure logic — no I/O, no time, no shared state. Covers the three design-doc outcomes: target reached (overshoot bounded by last-added candidate), undershoot tolerated (only with explicit positive tolerance_kw), and insufficient curtailable load (with a structured InsufficientLoadDetail the handler can echo back). Store layer: - interfaces/curtailment.go defines the org-scoped CurtailmentStore; v1 surface is the minimum needed to support Preview plus the basic event/target CRUD primitives so store tests can verify the schema constraints round-trip. - sqlstores/curtailment.go implements the interface using the sqlc- generated queries (GetCurtailmentOrgConfig, ListActiveCurtailedDevicesByOrg, ListRecentlyResolvedCurtailedDevicesByOrg, InsertCurtailmentEvent, GetCurtailmentEventByUUID, InsertCurtailmentTarget, ListCurtailmentTargetsByEvent, GetCurtailmentReconcilerHeartbeat). BE-1.x guard: - TestCurtailmentEventStateNumericPins asserts CANCELLED == 6 and FAILED == 7 at build time. The AdminTerminateEventRequest validator pins on (buf.validate.field).enum.in: [6, 7]; this test fails CI before any future enum reorder can silently desynchronize the validator. Selector + handler implementation lands in follow-up commits on this branch. Refs #140 Refs #118 Refs #173 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes #116.
Summary
Adds the kW-only curtailment foundation across proto/API contracts, SDK and plugin bridge support, capability flags, route registration, generated outputs, and initial plugin implementations. Fleet-level curtailment domain routes are intentionally stubbed, but direct plugin dispatch now supports explicit FULL curtailment and Proto efficiency curtailment.
This builds on command preflight filtering work from #110 so command handlers can reject unsafe requests before dispatch. The follow-up work will wire persistence, selection, reconciliation, and UI flows on top of these contracts.
What changed
Proto and API contracts
proto/curtailment/v1with Fleet-levelPreviewCurtailmentPlan,StartCurtailment,UpdateCurtailmentEvent,StopCurtailment,GetActiveCurtailment, andListCurtailmentEventsRPC definitions.CURTAILMENT_MODE_FIXED_KWwithFixedKwParams.CurtailmentLevelnow hasEFFICIENCY=1andFULL=2; Fleet event validation accepts FULL/default only for v1, while Proto direct device dispatch can advertise and use efficiency mode.FIXED_KWwithfixed_kw, least-efficient/default strategy, and normal/default/emergency priority.CommandCapabilitiesnow exposes onlycurtail_full_supportedandcurtail_efficiency_supported.SDK and gRPC bridge
CurtailLevel, optionalDeviceCurtailment, and curtail request payload plumbing to the Go SDK and generated gRPC driver bridge.CurtailandUncurtailrequest-shaped APIs with safe unsupported defaults.Server wiring
Plugin implementations
wasMining.was_mining.Generated outputs
Behavior changes
Reviewer guide
proto/curtailment/v1/curtailment.proto,proto/capabilities/v1/capabilities.proto, andserver/sdk/v1/pb/driver.proto.server/internal/handlers/curtailment.server/sdk/v1,server/internal/domain/plugins.plugin/virtual,plugin/antminer,plugin/proto, andplugin/asicrs.Out of scope
Test coverage