Enable topology response profiles for curtailment automation - #978
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 35b55208c9
ℹ️ 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.
Pull request overview
Enables building/rack/group (topology-scoped) response profiles to be bound to and executed by MQTT curtailment automation, by adding stricter live selector validation and strengthening the SQL-side execution fence (rule/profile/source/permissions/role) so automation fails closed when bindings or authorization drift.
Changes:
- Allow topology-scoped response profiles to be used by automation, with strict live selector validation on bind/execute paths.
- Add transactional locking/verification for MQTT source principal and role/permission rechecks during automation event insertion.
- Update ProtoFleet client “automation-ready” classification and associated unit tests to include topology scopes (while keeping unknown scopes read-only / not automation-ready).
Reviewed changes
Copilot reviewed 16 out of 21 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| server/sqlc/queries/user_organization.sql | Adds a locking query to fetch a user’s org role name for admin-role revalidation inside execution transactions. |
| server/sqlc/queries/curtailment_automation.sql | Extends the rule execution lock to also lock/verify the MQTT source row and its service user. |
| server/internal/domain/stores/sqlstores/response_profile_revision_integration_test.go | Adds integration coverage for topology automation execution failing closed on permission/role/source drift. |
| server/internal/domain/stores/sqlstores/response_profile_automation_scope_integration_test.go | Updates integration test expectations: topology-scoped profiles can be automated; deleted selectors fail closed. |
| server/internal/domain/stores/sqlstores/curtailment.go | Removes topology automation gating; adds topology selector locking/validation and new automation execution authorization fence. |
| server/internal/domain/curtailment/service.go | Removes the prior “topology Start not available for automation” gate. |
| server/internal/domain/curtailment/response_profile.go | Introduces ValidateAutomationScope for strict live validation in execution-capable paths. |
| server/internal/domain/curtailment/response_profile_test.go | Updates unit test to reflect that topology scopes are allowed even when automation rules exist. |
| server/internal/domain/curtailment/reconciler/dispatch_authorization.go | Refactors dispatch authorization to use the shared AuthorizationEnvelopeAllows helper. |
| server/internal/domain/curtailment/automation.go | Switches binding/execution checks to use ValidateAutomationScope and current-bound-profile validation. |
| server/internal/domain/curtailment/automation_test.go | Updates automation unit tests to allow topology binding/execution and to fail closed on unavailable selectors. |
| server/internal/domain/curtailment/authorization_envelope.go | Adds shared AuthorizationEnvelopeAllows helper for consistent admission/dispatch permission coverage checks. |
| server/generated/sqlc/user_organization.sql.go | Generated sqlc bindings for GetUserRoleNameForUpdate (generated — skip). |
| server/generated/sqlc/retrying_querier.gen.go | Generated retrying querier method for new query (generated — skip). |
| server/generated/sqlc/querier.go | Generated querier interface update (generated — skip). |
| server/generated/sqlc/db.go | Generated prepared-statement wiring for new query (generated — skip). |
| server/generated/sqlc/curtailment_automation.sql.go | Generated sqlc bindings for updated execution lock query (generated — skip). |
| client/src/protoFleet/features/settings/components/Curtailment/types.ts | Marks any recognized terminal scope type as automation-ready (no longer excluding topology scopes). |
| client/src/protoFleet/features/settings/components/Curtailment/CurtailmentSettingsPage.test.tsx | Updates settings page test to expect topology profiles to be automation-ready. |
| client/src/protoFleet/features/settings/components/Curtailment/CurtailmentAutomations.test.tsx | Updates automation UI tests to include topology-scoped profiles and keep unsupported scopes blocked. |
| client/src/protoFleet/api/useCurtailmentResponseProfiles.test.tsx | Updates hook tests for automation-ready classification (topology true; unknown false). |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
🔐 Codex Security Review
Review SummaryOverall Risk: HIGH Findings[HIGH] Automated review incomplete
NotesHuman review is required because the bounded automated review was incomplete. Generated by Codex Security Review | |
Reviewable diff: +377/-146 across 11 files (excludes generated, test, and story files).
Summary
Topology-scoped response profiles can now be bound to and executed by MQTT curtailment automation. Building, rack, and group selectors remain logical scopes, so each trigger resolves current membership while every automation scope fails closed if the selector, source principal, permissions, or required admin role changed. This is the automation-enablement slice of #909; the remaining picker/UI and E2E work stays tracked there.
How it works
When a rule is created, updated, enabled, or triggered, the response profile service strictly resolves its persisted topology selector. A new automation event enters the existing SQL execution transaction, which locks the rule, profile revision, MQTT source, current topology, effective permission assignments, and primary role before persisting any event or target reservation. If an OFF signal reasserts demand while an event is restoring, the re-curtail transaction applies the same rule, source-principal, permission, admin-role, profile, and event-ownership checks before it reclaims targets. The authorization-envelope evaluator is shared with the reconciler's pre-dispatch fence, keeping admission, re-curtail, and physical dispatch aligned.
flowchart LR A["MQTT OFF signal"] --> B["Load bound response profile"] B --> C{"Existing event is restoring?"} C -->|"No"| D["Resolve current building, rack, or group scope"] D --> E["Build curtailment plan"] E --> F["Transactional start fence"] C -->|"Yes"| G["Transactional re-curtail fence"] F --> H{"Binding and authorization still valid?"} G --> H H -->|"Yes"| I["Persist event or reclaim targets"] H -->|"No"| J["Reject without changing ownership"] I --> K["Reconciler revalidates before Curtail dispatch"]Areas of the code involved
client/curtailment profile typesserver/internal/domain/curtailmentserver/internal/domain/stores/sqlstoresserver/sqlc/queries/server/generated/sqlc/Key technical decisions & trade-offs
Testing & validation
go test ./internal/domain/curtailment/... ./internal/handlers/curtailmentgo test ./internal/domain/stores/sqlstores -run '^$'go build ./...fromserver/just _lint-servergo test ./cmd/fleetnode -run '^TestRunCmd_RefreshFailureCannotExtendControlStreamPastExpiry$' -count=10npx vitest runfor the response-profile hook, automation settings, and curtailment settings suites (89 tests)npx tsc --noEmitnpm run build:protoFleetjust _lint-clientfleetuser, so those cases are compiled locally and will execute in CI.Refs #909