fix(protocol): align quality ack schema
Verify Protocol / module (push) Successful in 1m21s
Verify Protocol / verify (push) Successful in 1m2s

This commit is contained in:
sechmachine
2026-08-13 07:53:35 +07:00
parent 6e18bc9ee6
commit c3ea2f35c6
7 changed files with 51 additions and 7 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ import (
"time" "time"
) )
const SchemaSHA256 = "8c1ed430127cf5774a919f14ff4770b2509d322c8518bf30c412e1e89eeffced" const SchemaSHA256 = "b9e72838b2ae02b3a47e7e2dfa03620b8580f2c98668e6eaf551ea8fb642d297"
const ProtocolVersion = "1.0.0" const ProtocolVersion = "1.0.0"
const CurrentWireVersion = "2" const CurrentWireVersion = "2"
const NMinus1WireVersion = "1" const NMinus1WireVersion = "1"
+1 -1
View File
@@ -14,5 +14,5 @@
}, },
"generator_sha256": "c5756780f59a82d8f56762e52f58e22a4d6aca6648157f45e5bad6559b9deeec", "generator_sha256": "c5756780f59a82d8f56762e52f58e22a4d6aca6648157f45e5bad6559b9deeec",
"protocol_version": "1.0.0", "protocol_version": "1.0.0",
"schema_sha256": "8c1ed430127cf5774a919f14ff4770b2509d322c8518bf30c412e1e89eeffced" "schema_sha256": "b9e72838b2ae02b3a47e7e2dfa03620b8580f2c98668e6eaf551ea8fb642d297"
} }
+1 -1
View File
@@ -1,6 +1,6 @@
// Code generated by tools/generate.py; DO NOT EDIT. // Code generated by tools/generate.py; DO NOT EDIT.
#![allow(non_snake_case)] #![allow(non_snake_case)]
pub const SCHEMA_SHA256: &str = "8c1ed430127cf5774a919f14ff4770b2509d322c8518bf30c412e1e89eeffced"; pub const SCHEMA_SHA256: &str = "b9e72838b2ae02b3a47e7e2dfa03620b8580f2c98668e6eaf551ea8fb642d297";
pub const CURRENT_WIRE_VERSION: &str = "2"; pub const CURRENT_WIRE_VERSION: &str = "2";
pub const N_MINUS_1_WIRE_VERSION: &str = "1"; pub const N_MINUS_1_WIRE_VERSION: &str = "1";
pub const N_MINUS_2_WIRE_VERSION: &str = "0"; pub const N_MINUS_2_WIRE_VERSION: &str = "0";
+1 -1
View File
@@ -1,7 +1,7 @@
// Code generated by tools/generate.py; DO NOT EDIT. // Code generated by tools/generate.py; DO NOT EDIT.
import Foundation import Foundation
public typealias JSONObject = [String: String] public typealias JSONObject = [String: String]
public let schemaSHA256 = "8c1ed430127cf5774a919f14ff4770b2509d322c8518bf30c412e1e89eeffced" public let schemaSHA256 = "b9e72838b2ae02b3a47e7e2dfa03620b8580f2c98668e6eaf551ea8fb642d297"
public let currentWireVersion = "2" public let currentWireVersion = "2"
public let nMinus1WireVersion = "1" public let nMinus1WireVersion = "1"
public let nMinus2WireVersion = "0" public let nMinus2WireVersion = "0"
+1 -1
View File
@@ -440,7 +440,7 @@ paths:
post: post:
tags: [gateway] tags: [gateway]
operationId: acknowledgeGatewayQualityWork operationId: acknowledgeGatewayQualityWork
description: The authenticated gateway mTLS certificate identity MUST match `gateway_id`; Server authority MUST exactly match the session, gateway, reconnect sequence, operation, revision, and monotonic lease generation tuple. Stale lease generations MUST be rejected. Maximum JSON body: 16384 bytes. description: The authenticated gateway mTLS certificate identity MUST match `gateway_id`; Server authority MUST exactly match the session, gateway, reconnect sequence, operation, revision, and monotonic lease generation tuple. Stale lease generations MUST be rejected. `applied` requires `current_applied_revision == revision`; `proven_prior` requires `current_applied_revision < revision`; `unknown` forbids `current_applied_revision` and makes no applied-revision assertion. Maximum JSON body: 16384 bytes.
security: [{gatewayMutualTLS: []}] security: [{gatewayMutualTLS: []}]
requestBody: {required: true, description: 'Maximum JSON body: 16384 bytes.', content: {application/json: {schema: {$ref: ../schemas/control-v1.schema.json#/$defs/GatewayQualityAck}}}} requestBody: {required: true, description: 'Maximum JSON body: 16384 bytes.', content: {application/json: {schema: {$ref: ../schemas/control-v1.schema.json#/$defs/GatewayQualityAck}}}}
responses: responses:
+6
View File
@@ -740,7 +740,13 @@
"GatewayQualityAck": { "GatewayQualityAck": {
"type": "object", "additionalProperties": false, "type": "object", "additionalProperties": false,
"x-max-bytes": 16384, "x-max-bytes": 16384,
"description": "Outcome invariants: applied requires current_applied_revision equal to revision; proven_prior requires current_applied_revision strictly less than revision; unknown forbids current_applied_revision and makes no applied-revision assertion.",
"required": ["version", "session_id", "gateway_id", "reconnect_sequence", "operation_id", "revision", "lease_generation", "outcome"], "required": ["version", "session_id", "gateway_id", "reconnect_sequence", "operation_id", "revision", "lease_generation", "outcome"],
"oneOf": [
{"properties": {"outcome": {"const": "applied"}}, "required": ["current_applied_revision"]},
{"properties": {"outcome": {"const": "proven_prior"}}, "required": ["current_applied_revision"]},
{"properties": {"outcome": {"const": "unknown"}}, "not": {"required": ["current_applied_revision"]}}
],
"properties": { "properties": {
"version": {"type": "string", "const": "1"}, "session_id": {"type": "string", "minLength": 1, "maxLength": 128}, "version": {"type": "string", "const": "1"}, "session_id": {"type": "string", "minLength": 1, "maxLength": 128},
"gateway_id": {"type": "string", "minLength": 1, "maxLength": 128}, "reconnect_sequence": {"type": "integer", "minimum": 0}, "gateway_id": {"type": "string", "minLength": 1, "maxLength": 128}, "reconnect_sequence": {"type": "integer", "minimum": 0},
+40 -2
View File
@@ -12,6 +12,17 @@ import sys
ROOT = pathlib.Path(__file__).resolve().parents[1] ROOT = pathlib.Path(__file__).resolve().parents[1]
def matches_outcome_branch(instance: dict[str, object], branch: dict[str, object]) -> bool:
required = branch.get("required", [])
if not all(field in instance for field in required):
return False
outcome = branch.get("properties", {}).get("outcome", {}).get("const")
if instance.get("outcome") != outcome:
return False
forbidden = branch.get("not", {}).get("required", [])
return not all(field in instance for field in forbidden) if forbidden else True
def main() -> int: def main() -> int:
schema_path = ROOT / "schemas/control-v1.schema.json" schema_path = ROOT / "schemas/control-v1.schema.json"
schema = json.loads(schema_path.read_text(encoding="utf-8")) schema = json.loads(schema_path.read_text(encoding="utf-8"))
@@ -59,8 +70,32 @@ def main() -> int:
assert quality_request["properties"]["lease_generation"]["minimum"] == 1 assert quality_request["properties"]["lease_generation"]["minimum"] == 1
assert defs["GatewayStopWorkRequest"]["required"] == ["version", "session_id", "gateway_id", "reconnect_sequence", "acquisition"] assert defs["GatewayStopWorkRequest"]["required"] == ["version", "session_id", "gateway_id", "reconnect_sequence", "acquisition"]
assert defs["GatewayQualityWork"]["required"][-3:] == ["lease_generation", "lease_expires_at", "selected_descriptor"] assert defs["GatewayQualityWork"]["required"][-3:] == ["lease_generation", "lease_expires_at", "selected_descriptor"]
assert defs["GatewayQualityAck"]["required"][-3:] == ["revision", "lease_generation", "outcome"] quality_ack = defs["GatewayQualityAck"]
assert defs["GatewayQualityAck"]["properties"]["outcome"]["enum"] == ["applied", "proven_prior", "unknown"] assert quality_ack["required"][-3:] == ["revision", "lease_generation", "outcome"]
assert quality_ack["properties"]["outcome"]["enum"] == ["applied", "proven_prior", "unknown"]
assert quality_ack["description"] == (
"Outcome invariants: applied requires current_applied_revision equal to revision; "
"proven_prior requires current_applied_revision strictly less than revision; "
"unknown forbids current_applied_revision and makes no applied-revision assertion."
)
assert quality_ack["oneOf"] == [
{"properties": {"outcome": {"const": "applied"}}, "required": ["current_applied_revision"]},
{"properties": {"outcome": {"const": "proven_prior"}}, "required": ["current_applied_revision"]},
{"properties": {"outcome": {"const": "unknown"}}, "not": {"required": ["current_applied_revision"]}},
]
ack_branches = quality_ack["oneOf"]
for valid_ack in (
{"outcome": "applied", "current_applied_revision": 7},
{"outcome": "proven_prior", "current_applied_revision": 6},
{"outcome": "unknown"},
):
assert sum(matches_outcome_branch(valid_ack, branch) for branch in ack_branches) == 1, valid_ack
for invalid_ack in (
{"outcome": "applied"},
{"outcome": "proven_prior"},
{"outcome": "unknown", "current_applied_revision": 6},
):
assert not any(matches_outcome_branch(invalid_ack, branch) for branch in ack_branches), invalid_ack
operation_id_pattern = r"^(?!00000000-0000-0000-0000-000000000000$)[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" operation_id_pattern = r"^(?!00000000-0000-0000-0000-000000000000$)[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
canonical_time_pattern = r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]{0,8}[1-9])?Z$" canonical_time_pattern = r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]{0,8}[1-9])?Z$"
@@ -235,6 +270,9 @@ def main() -> int:
assert "coordinates MUST match exactly" in quality_acquisition assert "coordinates MUST match exactly" in quality_acquisition
quality_acknowledgement = openapi.split(" operationId: acknowledgeGatewayQualityWork\n", 1)[1].split(" responses:\n", 1)[0] quality_acknowledgement = openapi.split(" operationId: acknowledgeGatewayQualityWork\n", 1)[1].split(" responses:\n", 1)[0]
assert "Stale lease generations MUST be rejected" in quality_acknowledgement assert "Stale lease generations MUST be rejected" in quality_acknowledgement
assert "`applied` requires `current_applied_revision == revision`" in quality_acknowledgement
assert "`proven_prior` requires `current_applied_revision < revision`" in quality_acknowledgement
assert "`unknown` forbids `current_applied_revision`" in quality_acknowledgement
for operation_id in ("createSessionQualityChange", "getSessionQualityChange", "createSessionStopOperation", "getSessionStopOperation"): for operation_id in ("createSessionQualityChange", "getSessionQualityChange", "createSessionStopOperation", "getSessionStopOperation"):
operation = openapi.split(f" operationId: {operation_id}\n", 1)[1].split(" responses:\n", 1)[0] operation = openapi.split(f" operationId: {operation_id}\n", 1)[1].split(" responses:\n", 1)[0]
assert "owning principal and active device/key" in operation, operation_id assert "owning principal and active device/key" in operation, operation_id