Skip to content

feat(curtailment): admin RPC, override fields, and session-only auth#173

Merged
rongxin-liu merged 8 commits intomainfrom
feat/issue-171-curtailment-admin-rpc-and-session-auth
May 6, 2026
Merged

feat(curtailment): admin RPC, override fields, and session-only auth#173
rongxin-liu merged 8 commits intomainfrom
feat/issue-171-curtailment-admin-rpc-and-session-auth

Conversation

@rongxin-liu
Copy link
Copy Markdown
Contributor

@rongxin-liu rongxin-liu commented May 5, 2026

Background

Curtailment is the proto-fleet feature that reduces a fleet's mining power on demand — operators can preview a plan, start a curtailment event for a selected scope of miners, verify the reduction via telemetry, and restore the miners safely afterwards. Use cases include grid-program participation, demand response, and operator-initiated power reduction.

The curtailment foundation (#116, merged in #118) shipped the v1 proto contract — six RPCs (PreviewCurtailmentPlan, StartCurtailment, UpdateCurtailmentEvent, StopCurtailment, GetActiveCurtailment, ListCurtailmentEvents), the full enum surface (modes, strategies, levels, priorities, event/target states), capability flags, and Connect handler stubs that all return Unimplemented. Persistence, the selector, the reconciler, the restorer, and the read-API logic land in subsequent tickets that fill in those stubs.

This PR is a contract-layer follow-up to that foundation: a small set of additions to the same proto / handler / interceptor surface that need to be in place before the downstream tickets wire up bodies. Every new RPC stub still returns Unimplemented; the value here is contract shape and auth scoping.

Summary

Four scoped changes:

  • AdminTerminateEvent RPC for the dead-reconciler operational runbook. Forces a non-terminal event to a terminal state. target_state CEL is restricted to CANCELLED (=6) and FAILED (=7); COMPLETED states are intentionally rejected so the recovery RPC cannot misreport an event whose restore did not actually run. Carries an idempotency_key field so operator double-clicks during the runbook collapse to a single recovery action.
  • Three admin-gated optional override request fields, each with buf.validate sanity ceilings:
    • optional uint32 candidate_min_power_w_override on PreviewCurtailmentPlanRequest and StartCurtailmentRequest — per-org default override for the candidate-eligibility floor; bounded to [1, 10_000_000] (10 MW per miner).
    • bool allow_unbounded on StartCurtailmentRequest — explicit acknowledgement to skip max_duration_default_sec normalization.
    • optional uint32 restore_batch_size_override on StopCurtailmentRequest — takes precedence over any prior UpdateCurtailmentEvent value for the duration of restore; bounded to [1, 10_000]. The "zero means use default" convention used elsewhere in the proto is preserved by the lower bound on each override.
  • Session-only registration of AdminTerminateEvent only. The recovery escape hatch must not be reachable via a long-lived bearer token. StartCurtailment / StopCurtailment / UpdateCurtailmentEvent and the read RPCs remain API-key-accessible so external integrations can drive curtailment via the public API. The session-only listing and the handler-side admin gate are now cross-referenced in code comments as a paired invariant — neither alone is sufficient.
  • CategoryCurtailment added to the activity event-category taxonomy so subsequent tickets can emit curtailment-domain events under a dedicated category.

Also: when the handler-side admin gate sees no session.Info in context, it now returns Unauthenticated instead of propagating Internal from session.GetInfo. The auth interceptor should prevent this in production, but if interceptor wiring ever regresses, the response code reflects "no identity" rather than "server bug" — quieter alerts, no retry-storm encouragement.

Security posture

RPC API-key auth? Admin role required?
PreviewCurtailmentPlan yes only when an override field is set
StartCurtailment yes only when an override field is set
StopCurtailment yes only when an override field is set
UpdateCurtailmentEvent yes no (current contract)
GetActiveCurtailment yes no
ListCurtailmentEvents yes no
AdminTerminateEvent no — session only yes

The handler-level admin gate (requireAdminFromContext) fires on AdminTerminateEvent always, and on Preview / Start / Stop when the corresponding override field is set on the request. The current API-key model is per-user with role inheritance, so an admin-role API key can drive override-bearing requests — matching how the apikey handler itself gates admin operations on role alone. A leaked admin API key already has full curtailment-write blast radius via plain StartCurtailment; restricting the override paths further would not close that gap.

AdminTerminateEvent's session-only registration narrows the blast radius for the operator-of-last-resort recovery escape hatch specifically. A future scoped-API-key primitive would be the right place to relax it.

What changed

Eight logical commits:

  1. feat(curtailment): add admin RPC and admin-gated override fields — proto contract additions, regenerated Go + TypeScript outputs.
  2. feat(activity): add curtailment event categoryCategoryCurtailment enum entry plus a table-driven test covering all three Valid() switches in activity/models.go (EventCategory, ActorType, ResultType).
  3. feat(curtailment): gate admin and override paths at the handler — single requireAdminFromContext helper called from AdminTerminateEvent (always) and from Preview / Start / Stop when the corresponding override field is set. Action-verb error messages are routed through two package-level constants (actionSupplyOverrideFields, actionTerminateEvents).
  4. feat(interceptors): register curtailment write RPCs as session-only — initial registration of all four write/admin procedures.
  5. feat(curtailment): allow API key access to write RPCs except admin recovery — narrows the previous commit so only AdminTerminateEvent remains session-only. Test surface updated to match: invert the role-gate matrix to assert admin-via-API-key reaches Unimplemented on override paths, expand the "API-key-accessible" assertion list, scope the runtime SessionOnly interceptor test to the single remaining session-only procedure.
  6. refactor(curtailment): trim handler doc and round out viewer × auth-method matrix — small post-review polish: trim a 4-line doc comment to one line and add three missing viewer-via-API-key cases on Start/Stop so the override-gate test matrix is symmetric across auth methods.
  7. refactor(curtailment): rename AdminTransitionEvent to AdminTerminateEvent — addresses the inline review nit. The RPC's target_state CEL is restricted to terminal states only, so Terminate describes operational behavior more accurately than Transition (which describes the underlying state-machine mechanism). Mechanical rename across the proto, generated outputs, handler, tests, and SessionOnly registration.
  8. refactor(curtailment): apply review feedback — review-driven hardening pass. Adds idempotency_key (= 4) to AdminTerminateEventRequest; adds buf.validate {gte: 1, lte: ...} sanity ceilings on the two override uint32 fields; remaps the handler-side admin gate's missing-session case from Internal to Unauthenticated (tests updated accordingly); cross-references the paired AdminTerminateEvent role gates in code comments at both sites so neither layer is removed independently.

What is intentionally not in this PR

  • Bodies for PreviewCurtailmentPlan, StartCurtailment, UpdateCurtailmentEvent, StopCurtailment, GetActiveCurtailment, ListCurtailmentEvents — those land in subsequent persistence, dispatch, and read-API tickets.
  • AdminTerminateEvent business logic — handler stub returns Unimplemented after the role gate. The transition logic and curtailment_admin_terminate activity emission land with the read-API ticket.
  • Selector consumption of candidate_min_power_w_override — the proto field exists; the persistence/preview ticket consumes it.
  • Adaptive batch sizing that restore_batch_size_override overrides — lands with the restore ticket.
  • Handler-side max_duration_default_sec normalization driven by allow_unbounded — lands with the start/dispatch ticket.
  • A scoped-API-key primitive that would let AdminTerminateEvent accept API-key callers with an explicit curtailment.admin scope — separate larger feature.
  • Consolidating role checks across the curtailment and apikey handlers into a shared domain/auth helper — separate small refactor PR.
  • End-to-end interceptor → handler chain integration test (each layer is unit-tested in isolation today; bundled E2E coverage is deferred).

Test plan

  • buf lint clean
  • npm run lint (eslint, max-warnings 0) clean
  • golangci-lint run clean across all three lint targets (server, plugin/proto, plugin/antminer)
  • Targeted Go tests pass for the changed packages:
    • go test ./internal/handlers/curtailment/...
    • go test ./internal/handlers/interceptors/...
    • go test ./internal/domain/activity/models/...
  • just gen regenerates Go + TypeScript outputs cleanly with no orphan diff (proto + generated outputs are in the same commit per AGENTS.md rule 2).
  • TestHandler_OverrideFieldsRoleGate covers 16 cases — the full (Preview / Start-candidate / Start-allow_unbounded / Stop) × (session / API-key) × (viewer / admin) matrix: viewer is rejected on every override path regardless of auth method; admin reaches Unimplemented on every override path regardless of auth method. Verifies the override gate is keyed on role, not on auth method.
  • TestHandler_NoOverrideSkipsRoleGate confirms Preview / Stop without overrides reach Unimplemented regardless of session info.
  • TestHandler_AdminTerminateEventRoleGate covers admin / super-admin / viewer / empty-role paths against AdminTerminateEvent directly.
  • TestHandler_AdminTerminateEventValidation covers the buf.validate constraints: target_state rejects all six non-CANCELLED/FAILED values (UNSPECIFIED, PENDING, ACTIVE, RESTORING, COMPLETED, COMPLETED_WITH_FAILURES); event_uuid and reason min_len=1 enforced. Validator-passed cases now surface CodeUnauthenticated from the handler-side admin gate (no session in test context), matching the remapped error code.
  • TestHandler_AdminTerminateEventRejectsMissingSession asserts the handler-side admin gate returns CodeUnauthenticated when no session.Info is in context.
  • TestAuthInterceptor_AdminTerminateEventRejectsApiKeyAuth exercises the runtime authenticate() path with a Bearer header against AdminTerminateEvent and asserts PermissionDenied.
  • TestCurtailmentAdminProcedureIsSessionOnly and TestCurtailmentNonAdminProceduresStayApiKeyAccessible pin the registration list both directions.

Closes #171
Refs #116
Refs #118

🤖 Generated with Claude Code

rongxin-liu and others added 4 commits May 5, 2026 14:12
Adds the proto contract surface for the BE-1.1 follow-up to issue #171:

- AdminTransitionEvent RPC with AdminTransitionEventRequest/Response messages
  for the dead-reconciler operational runbook. target_state CEL is restricted
  to CANCELLED (=6) and FAILED (=7); COMPLETED states are intentionally
  excluded so the recovery RPC cannot misreport an event whose restore did
  not actually run.
- StartCurtailmentRequest gains optional candidate_min_power_w_override
  (per-org default override) and bool allow_unbounded (skip
  max_duration_default_sec normalization). Both are admin-only.
- PreviewCurtailmentPlanRequest gains optional candidate_min_power_w_override
  on the same field tag (26).
- StopCurtailmentRequest gains optional restore_batch_size_override that
  takes precedence over any prior UpdateCurtailmentEvent value for the
  duration of restore.

Generated Go and TypeScript outputs regenerated via just gen.

Refs: #171, #116, #118

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds CategoryCurtailment to the EventCategory enum used by the activity
log so curtailment domain code in subsequent tickets (BE-3 dispatch,
BE-5 read APIs) can emit events under a dedicated category rather than
overloading an existing one.

Also lands a table-driven test covering all three Valid() switches in
this file (EventCategory, ActorType, ResultType). The previous file had
no Valid() coverage; pinning all three together prevents an
"added a new enum value but forgot to extend Valid()" regression for
future curtailment, schedule, or auth additions.

Refs: #171

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lands the handler-layer enforcement for BE-1.1's admin recovery RPC and
the three admin-gated optional override request fields, plus the
matching test surface.

requireAdminFromContext rejects non-admin roles AND API-key auth before
returning Unimplemented from the stub. The API-key rejection hardens
the override path on PreviewCurtailmentPlan, which is otherwise
API-key-accessible — a leaked admin-owner API key cannot drive
override-bearing Preview calls. Start/Stop/Update/AdminTransition are
already covered at the interceptor layer (next commit), so the
handler-level API-key check is defense-in-depth for those four.

Test coverage:
- TestHandler_AdminTransitionEventRoleGate: admin/super-admin reach
  Unimplemented; viewer + empty role return PermissionDenied.
- TestHandler_AdminTransitionEventValidation: buf.validate constraints
  on event_uuid, target_state (CANCELLED/FAILED only), and reason.
- TestHandler_OverrideFieldsRoleGate: 11 cases covering the matrix of
  (override field) × (admin-via-API-key | admin-via-session | viewer).
  API-key with admin role is rejected to prevent escape-hatch escalation.
- TestHandler_NoOverrideSkipsRoleGate: Preview/Stop without overrides
  reach Unimplemented (preserves API-key-accessible reads).
- TestHandler_NonAdminRPCsReturnUnimplemented: renamed from
  TestHandler_AllRPCsReturnUnimplemented since AdminTransitionEvent's
  Unimplemented body is covered by the role-gate test instead.
- TestHandler_AdminTransitionEventRejectsMissingSession: covers the
  RPC invoked outside the authenticated request path.

Refs: #171

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds StartCurtailment, StopCurtailment, UpdateCurtailmentEvent, and
AdminTransitionEvent to SessionOnlyProcedures so a leaked API key
cannot mass-stop a fleet, abort a live curtailment event, or
force-recover a non-terminal event. The four read RPCs
(PreviewCurtailmentPlan, GetActiveCurtailment, ListCurtailmentEvents)
remain API-key-accessible for monitoring and dashboards.

Three tests pin the contract:
- TestCurtailmentWriteProceduresAreSessionOnly: list-membership
  assertion that the four write/admin RPCs are present.
- TestCurtailmentReadProceduresStayApiKeyAccessible: list-membership
  assertion that the three read RPCs are NOT present.
- TestAuthInterceptor_SessionOnlyRejectsApiKeyAuth: runtime test
  driving authenticate() with a Bearer header against each session-only
  curtailment procedure and asserting PermissionDenied. Closes the
  issue #171 acceptance criterion that called for runtime enforcement
  coverage rather than list-only assertions.

Refs: #171

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 5, 2026 13:15
@rongxin-liu rongxin-liu requested a review from a team as a code owner May 5, 2026 13:15
@github-actions github-actions Bot added javascript Pull requests that update javascript code client server shared labels May 5, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds the remaining contract/auth hardening for curtailment v1 ahead of persistence/dispatcher work: a new admin recovery RPC, admin-gated override request fields, and session-only enforcement for curtailment write/admin procedures, plus an activity taxonomy update to support curtailment-domain events.

Changes:

  • Added AdminTransitionEvent RPC to the curtailment proto (with validation restricting target_state to CANCELLED/FAILED) and updated generated Go/TypeScript outputs.
  • Enforced session-only auth for curtailment write/admin RPCs in the auth interceptor config, with tests pinning both membership and runtime behavior.
  • Added handler-level admin gating for override-bearing requests and for AdminTransitionEvent, plus tests for role/auth-method matrices and request validation.

Reviewed changes

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

Show a summary per file
File Description
server/internal/handlers/interceptors/config.go Adds curtailment write/admin procedures to SessionOnlyProcedures.
server/internal/handlers/interceptors/config_test.go Tests session-only list membership, read-RPC non-membership, and runtime API-key rejection.
server/internal/handlers/curtailment/handler.go Adds AdminTransitionEvent stub + central requireAdminFromContext gate for admin-only paths/override fields.
server/internal/handlers/curtailment/handler_test.go Adds targeted tests for admin role gating, override-field gating, and AdminTransitionEvent validation.
server/internal/domain/activity/models/models.go Introduces CategoryCurtailment and marks it valid.
server/internal/domain/activity/models/models_test.go Table-driven tests covering Valid() for EventCategory, ActorType, and ResultType.
proto/curtailment/v1/curtailment.proto Adds new RPC + override fields and validation constraints.
server/generated/grpc/curtailment/v1/curtailmentv1connect/curtailment.connect.go Regenerated Connect stubs to include the new RPC (generated).
client/src/protoFleet/api/generated/curtailment/v1/curtailment_pb.ts Regenerated TS protobuf outputs for new fields/RPC (generated).

…covery

Drops StartCurtailment, StopCurtailment, and UpdateCurtailmentEvent from
SessionOnlyProcedures so external integrations can drive curtailment via
the public API. AdminTransitionEvent stays session-only — the operator-
of-last-resort recovery RPC must not be reachable via a long-lived
bearer token.

The handler-level role gate (requireAdminFromContext) on the override
paths is preserved but no longer rejects API-key auth specifically.
Override fields still require admin / super-admin role; the API key model
is per-user with role inheritance, so an admin-role API key can drive
override-bearing requests. Adding extra friction on the API-key auth
method while plain Start / Stop calls remain API-key-accessible buys no
real defense — a leaked admin key has full curtailment-write blast
radius either way.

Test changes:
- TestCurtailmentWriteProceduresAreSessionOnly →
  TestCurtailmentAdminProcedureIsSessionOnly: only AdminTransitionEvent.
- TestCurtailmentReadProceduresStayApiKeyAccessible →
  TestCurtailmentNonAdminProceduresStayApiKeyAccessible: now also
  asserts Start / Stop / Update are NOT in SessionOnlyProcedures.
- TestAuthInterceptor_SessionOnlyRejectsApiKeyAuth →
  TestAuthInterceptor_AdminTransitionEventRejectsApiKeyAuth: scoped to
  the single remaining session-only curtailment procedure.
- TestHandler_OverrideFieldsRoleGate: API-key cases inverted — admin via
  API key now reaches Unimplemented; viewer is rejected regardless of
  auth method. New "viewer + API key" case added for parity.

Refs: #171

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

github-actions Bot commented May 5, 2026

🔐 Codex Security Review

Note: This is an automated security-focused code review generated by Codex.
It should be used as a supplementary check alongside human review.
False positives are possible - use your judgment.

Scope summary

  • Reviewed pull request diff only (fb12730479631adb4bc973182d42d5767a54a9a0...58a2a55a795b2f3316dcbff4e232a49bee9558a8, exact PR three-dot diff)
  • Model: gpt-5.4

💡 Click "edited" above to see previous reviews for this PR.


Review Summary

Overall Risk: MEDIUM

Findings

[MEDIUM] allow_unbounded is only protected on start, leaving the update path as a likely bypass

  • Category: Auth
  • Location: server/internal/handlers/curtailment/handler.go:42
  • Description: This change introduces allow_unbounded as an admin-only acknowledgement on StartCurtailment, but UpdateCurtailmentEvent remains unchanged and still exposes max_duration_seconds with no corresponding admin-only acknowledgement or guard. That makes the new safety boundary creation-only: once UpdateCurtailmentEvent gets implemented, a caller that is allowed to update an event can likely clear the duration cap after the fact by setting max_duration_seconds to 0.
  • Impact: The new per-org max-duration safeguard can be bypassed, allowing an event to remain unbounded and miners to stay curtailed indefinitely. In practice that is an availability and revenue-loss issue, and the bypass would still sit on an API-key-accessible RPC surface unless update semantics are tightened too.
  • Recommendation: Mirror the allow_unbounded control on UpdateCurtailmentEvent, or normalize max_duration_seconds=0 to the org default on updates as well. If unbounded updates are ever allowed, gate them with the same admin-only policy as start.

[LOW] The new override fields document 0 as “use default”, but validation rejects explicit 0

  • Category: Protobuf
  • Location: proto/curtailment/v1/curtailment.proto:434
  • Description: The new override fields are documented as if 0 means “use default”/“excluded”, but they are declared as optional fields with gte: 1 validation. That means any client that explicitly sends 0 instead of omitting the field will get InvalidArgument rather than default behavior. The same pattern appears on candidate_min_power_w_override and restore_batch_size_override.
  • Impact: Manual Connect/JSON clients, automation, or future SDKs that follow the comment literally can fail unexpectedly at runtime, which is a correctness and interoperability risk for the new API surface.
  • Recommendation: Either accept 0 and normalize it server-side, or update the comments/docs to say the field must be omitted to use the default.

Notes

Within the reviewed diff, I did not see new SQL, shell execution, network discovery, plugin, Rust, frontend rendering, or infrastructure changes. The scope here is protobuf/API surface, generated code, auth scaffolding, and activity-model constants.

The new AdminTerminateEvent RPC itself looks appropriately constrained in this diff: it is added to SessionOnlyProcedures, and its proto validation limits target_state to CANCELLED or FAILED.


Generated by Codex Security Review |
Triggered by: @rongxin-liu |
Review workflow run

…ethod matrix

Two small simplifications surfaced by the post-API-access review:

- Trim requireAdminFromContext doc to a one-line behavioral summary; the
  why-no-auth-method-check rationale lives in the prior commit message.
- Add three missing TestHandler_OverrideFieldsRoleGate cases so viewer ×
  API-key is exercised on Start (candidate + allow_unbounded) and Stop,
  matching the existing Preview coverage. Pins the contract that the
  override gate rejects viewer regardless of auth method.

Refs: #171

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread proto/curtailment/v1/curtailment.proto Outdated
…vent

Reviewer nit on #173: the RPC's target_state CEL is restricted to
CANCELLED and FAILED, so the operational behavior is "force-end this
event" rather than "transition through a state graph". Rename the RPC,
its Request/Response messages, the action-verb error string, and the
matching test function names so the public surface describes behavior
rather than mechanism. Mechanical rename only; no logic change.

Generated Go and TypeScript outputs regenerated via just gen.

Refs: #171

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add idempotency_key (= 4) to AdminTerminateEventRequest, mirroring
  StartCurtailmentRequest semantics so recovery RPC retries collapse.
- Add buf.validate bounds on the admin override fields:
  candidate_min_power_w_override (gte: 1, lte: 10000000) and
  restore_batch_size_override (gte: 1, lte: 10000); zero-as-override is
  excluded since the existing convention is "zero means use default".
- Cross-reference the paired AdminTerminateEvent role gates so neither
  side (SessionOnlyProcedures entry, handler-side requireAdminFromContext)
  is removed independently.
- Remap missing session.Info from CodeInternal to CodeUnauthenticated in
  requireAdminFromContext so the response code reflects "no identity"
  rather than "server bug"; tests updated to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@rongxin-liu rongxin-liu merged commit 9890485 into main May 6, 2026
66 checks passed
@rongxin-liu rongxin-liu deleted the feat/issue-171-curtailment-admin-rpc-and-session-auth branch May 6, 2026 11:45
rongxin-liu added a commit that referenced this pull request May 6, 2026
…-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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

client javascript Pull requests that update javascript code server shared

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add curtailment admin RPC, override fields, and session-only auth

3 participants