fix(protocol): fence gateway work recovery

This commit is contained in:
sechmachine
2026-08-13 07:43:42 +07:00
parent 8eacc4fda9
commit 6e18bc9ee6
21 changed files with 572 additions and 146 deletions
+71 -27
View File
@@ -14,7 +14,7 @@ import (
"time"
)
const SchemaSHA256 = "614fa11dd1f49b8e10bf21b8c10eadbc1468d16bfcc6ed0a26d29671a5e61300"
const SchemaSHA256 = "8c1ed430127cf5774a919f14ff4770b2509d322c8518bf30c412e1e89eeffced"
const ProtocolVersion = "1.0.0"
const CurrentWireVersion = "2"
const NMinus1WireVersion = "1"
@@ -330,8 +330,9 @@ type GatewayQualityAck struct {
ReconnectSequence int64 `json:"reconnect_sequence"`
OperationID string `json:"operation_id"`
Revision int64 `json:"revision"`
LeaseGeneration int64 `json:"lease_generation"`
Outcome string `json:"outcome"`
CurrentAppliedRevision int64 `json:"current_applied_revision"`
CurrentAppliedRevision *int64 `json:"current_applied_revision,omitempty"`
FailureCode string `json:"failure_code,omitempty"`
}
@@ -342,6 +343,7 @@ type GatewayQualityWork struct {
ReconnectSequence int64 `json:"reconnect_sequence"`
OperationID string `json:"operation_id"`
Revision int64 `json:"revision"`
LeaseGeneration int64 `json:"lease_generation"`
LeaseExpiresAt string `json:"lease_expires_at"`
SelectedDescriptor SelectedSessionDescriptor `json:"selected_descriptor"`
CurrentAppliedRevision *int64 `json:"current_applied_revision,omitempty"`
@@ -352,8 +354,10 @@ type GatewayQualityWorkRequest struct {
SessionID string `json:"session_id"`
GatewayID string `json:"gateway_id"`
ReconnectSequence int64 `json:"reconnect_sequence"`
OperationID string `json:"operation_id"`
Revision int64 `json:"revision"`
Acquisition string `json:"acquisition"`
OperationID string `json:"operation_id,omitempty"`
Revision *int64 `json:"revision,omitempty"`
LeaseGeneration *int64 `json:"lease_generation,omitempty"`
CurrentAppliedRevision *int64 `json:"current_applied_revision,omitempty"`
}
@@ -397,7 +401,8 @@ type GatewayStopWorkRequest struct {
SessionID string `json:"session_id"`
GatewayID string `json:"gateway_id"`
ReconnectSequence int64 `json:"reconnect_sequence"`
OperationID string `json:"operation_id"`
Acquisition string `json:"acquisition"`
OperationID string `json:"operation_id,omitempty"`
}
type GatewayTelemetry struct {
@@ -3487,13 +3492,19 @@ func (v GatewayQualityAck) Validate() error {
if v.Revision != 0 && v.Revision < 1 {
violations = append(violations, FieldViolation{Field: "revision", Code: "minimum"})
}
if v.LeaseGeneration == 0 {
violations = append(violations, FieldViolation{Field: "lease_generation", Code: "required"})
}
if v.LeaseGeneration != 0 && v.LeaseGeneration < 1 {
violations = append(violations, FieldViolation{Field: "lease_generation", Code: "minimum"})
}
if v.Outcome == "" {
violations = append(violations, FieldViolation{Field: "outcome", Code: "required"})
}
if v.Outcome != "" && !(v.Outcome == "applied" || v.Outcome == "not_applied" || v.Outcome == "uncertain") {
if v.Outcome != "" && !(v.Outcome == "applied" || v.Outcome == "proven_prior" || v.Outcome == "unknown") {
violations = append(violations, FieldViolation{Field: "outcome", Code: "invalid_value"})
}
if v.CurrentAppliedRevision != 0 && v.CurrentAppliedRevision < 0 {
if v.CurrentAppliedRevision != nil && *v.CurrentAppliedRevision != 0 && *v.CurrentAppliedRevision < 0 {
violations = append(violations, FieldViolation{Field: "current_applied_revision", Code: "minimum"})
}
if len(v.FailureCode) < 1 && v.FailureCode != "" {
@@ -3502,6 +3513,15 @@ func (v GatewayQualityAck) Validate() error {
if len(v.FailureCode) > 128 {
violations = append(violations, FieldViolation{Field: "failure_code", Code: "max_length"})
}
if v.Outcome == "applied" && (v.CurrentAppliedRevision == nil || *v.CurrentAppliedRevision != v.Revision) {
violations = append(violations, FieldViolation{Field: "current_applied_revision", Code: "invalid_tagged_value"})
}
if v.Outcome == "proven_prior" && (v.CurrentAppliedRevision == nil || *v.CurrentAppliedRevision >= v.Revision) {
violations = append(violations, FieldViolation{Field: "current_applied_revision", Code: "invalid_tagged_value"})
}
if v.Outcome == "unknown" && v.CurrentAppliedRevision != nil {
violations = append(violations, FieldViolation{Field: "current_applied_revision", Code: "invalid_tagged_value"})
}
if len(violations) > 0 {
return ValidationError{Violations: violations}
}
@@ -3520,12 +3540,12 @@ func DecodeGatewayQualityAck(data []byte) (GatewayQualityAck, error) {
if err := json.Unmarshal(data, &fields); err != nil {
return value, err
}
if raw, ok := fields["current_applied_revision"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "current_applied_revision", Code: "required"}}}
}
if raw, ok := fields["gateway_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "gateway_id", Code: "required"}}}
}
if raw, ok := fields["lease_generation"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "lease_generation", Code: "required"}}}
}
if raw, ok := fields["operation_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "operation_id", Code: "required"}}}
}
@@ -3616,6 +3636,12 @@ func (v GatewayQualityWork) Validate() error {
if v.Revision != 0 && v.Revision < 1 {
violations = append(violations, FieldViolation{Field: "revision", Code: "minimum"})
}
if v.LeaseGeneration == 0 {
violations = append(violations, FieldViolation{Field: "lease_generation", Code: "required"})
}
if v.LeaseGeneration != 0 && v.LeaseGeneration < 1 {
violations = append(violations, FieldViolation{Field: "lease_generation", Code: "minimum"})
}
if v.LeaseExpiresAt == "" {
violations = append(violations, FieldViolation{Field: "lease_expires_at", Code: "required"})
}
@@ -3660,6 +3686,9 @@ func DecodeGatewayQualityWork(data []byte) (GatewayQualityWork, error) {
if raw, ok := fields["lease_expires_at"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "lease_expires_at", Code: "required"}}}
}
if raw, ok := fields["lease_generation"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "lease_generation", Code: "required"}}}
}
if raw, ok := fields["operation_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "operation_id", Code: "required"}}}
}
@@ -3732,8 +3761,11 @@ func (v GatewayQualityWorkRequest) Validate() error {
if v.ReconnectSequence != 0 && v.ReconnectSequence < 0 {
violations = append(violations, FieldViolation{Field: "reconnect_sequence", Code: "minimum"})
}
if v.OperationID == "" {
violations = append(violations, FieldViolation{Field: "operation_id", Code: "required"})
if v.Acquisition == "" {
violations = append(violations, FieldViolation{Field: "acquisition", Code: "required"})
}
if v.Acquisition != "" && !(v.Acquisition == "poll" || v.Acquisition == "prompt" || v.Acquisition == "observation") {
violations = append(violations, FieldViolation{Field: "acquisition", Code: "invalid_value"})
}
if len(v.OperationID) < 36 && v.OperationID != "" {
violations = append(violations, FieldViolation{Field: "operation_id", Code: "min_length"})
@@ -3744,15 +3776,24 @@ func (v GatewayQualityWorkRequest) Validate() error {
if v.OperationID != "" && !validCanonicalUUID(v.OperationID) {
violations = append(violations, FieldViolation{Field: "operation_id", Code: "invalid_uuid"})
}
if v.Revision == 0 {
violations = append(violations, FieldViolation{Field: "revision", Code: "required"})
}
if v.Revision != 0 && v.Revision < 1 {
if v.Revision != nil && *v.Revision != 0 && *v.Revision < 1 {
violations = append(violations, FieldViolation{Field: "revision", Code: "minimum"})
}
if v.LeaseGeneration != nil && *v.LeaseGeneration != 0 && *v.LeaseGeneration < 1 {
violations = append(violations, FieldViolation{Field: "lease_generation", Code: "minimum"})
}
if v.CurrentAppliedRevision != nil && *v.CurrentAppliedRevision != 0 && *v.CurrentAppliedRevision < 0 {
violations = append(violations, FieldViolation{Field: "current_applied_revision", Code: "minimum"})
}
if v.Acquisition == "poll" && (v.OperationID != "" || v.Revision != nil || v.LeaseGeneration != nil || v.CurrentAppliedRevision != nil) {
violations = append(violations, FieldViolation{Field: "acquisition", Code: "invalid_tagged_value"})
}
if v.Acquisition == "prompt" && (v.OperationID == "" || v.Revision == nil || v.LeaseGeneration != nil || v.CurrentAppliedRevision != nil) {
violations = append(violations, FieldViolation{Field: "acquisition", Code: "invalid_tagged_value"})
}
if v.Acquisition == "observation" && (v.OperationID == "" || v.Revision == nil || v.LeaseGeneration == nil || v.CurrentAppliedRevision == nil) {
violations = append(violations, FieldViolation{Field: "acquisition", Code: "invalid_tagged_value"})
}
if len(violations) > 0 {
return ValidationError{Violations: violations}
}
@@ -3771,18 +3812,15 @@ func DecodeGatewayQualityWorkRequest(data []byte) (GatewayQualityWorkRequest, er
if err := json.Unmarshal(data, &fields); err != nil {
return value, err
}
if raw, ok := fields["acquisition"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "acquisition", Code: "required"}}}
}
if raw, ok := fields["gateway_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "gateway_id", Code: "required"}}}
}
if raw, ok := fields["operation_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "operation_id", Code: "required"}}}
}
if raw, ok := fields["reconnect_sequence"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "reconnect_sequence", Code: "required"}}}
}
if raw, ok := fields["revision"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "revision", Code: "required"}}}
}
if raw, ok := fields["session_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "session_id", Code: "required"}}}
}
@@ -4273,8 +4311,11 @@ func (v GatewayStopWorkRequest) Validate() error {
if v.ReconnectSequence != 0 && v.ReconnectSequence < 0 {
violations = append(violations, FieldViolation{Field: "reconnect_sequence", Code: "minimum"})
}
if v.OperationID == "" {
violations = append(violations, FieldViolation{Field: "operation_id", Code: "required"})
if v.Acquisition == "" {
violations = append(violations, FieldViolation{Field: "acquisition", Code: "required"})
}
if v.Acquisition != "" && !(v.Acquisition == "poll" || v.Acquisition == "prompt") {
violations = append(violations, FieldViolation{Field: "acquisition", Code: "invalid_value"})
}
if len(v.OperationID) < 36 && v.OperationID != "" {
violations = append(violations, FieldViolation{Field: "operation_id", Code: "min_length"})
@@ -4285,6 +4326,9 @@ func (v GatewayStopWorkRequest) Validate() error {
if v.OperationID != "" && !validCanonicalUUID(v.OperationID) {
violations = append(violations, FieldViolation{Field: "operation_id", Code: "invalid_uuid"})
}
if v.Acquisition == "poll" && v.OperationID != "" || v.Acquisition == "prompt" && v.OperationID == "" {
violations = append(violations, FieldViolation{Field: "acquisition", Code: "invalid_tagged_value"})
}
if len(violations) > 0 {
return ValidationError{Violations: violations}
}
@@ -4303,12 +4347,12 @@ func DecodeGatewayStopWorkRequest(data []byte) (GatewayStopWorkRequest, error) {
if err := json.Unmarshal(data, &fields); err != nil {
return value, err
}
if raw, ok := fields["acquisition"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "acquisition", Code: "required"}}}
}
if raw, ok := fields["gateway_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "gateway_id", Code: "required"}}}
}
if raw, ok := fields["operation_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "operation_id", Code: "required"}}}
}
if raw, ok := fields["reconnect_sequence"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "reconnect_sequence", Code: "required"}}}
}