From a19023995e0f91ddfe865945117093e6b877f492 Mon Sep 17 00:00:00 2001 From: sechmachine <97589681+sechmachine727@users.noreply.github.com> Date: Thu, 13 Aug 2026 13:43:07 +0700 Subject: [PATCH] fix(protocol): bind relaunch termination authority --- openapi/control-v1.yaml | 14 +++++++++----- tools/validate.py | 33 +++++++++++++++++++++------------ 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/openapi/control-v1.yaml b/openapi/control-v1.yaml index dca4ecf..2f8c786 100644 --- a/openapi/control-v1.yaml +++ b/openapi/control-v1.yaml @@ -311,12 +311,16 @@ paths: '202': description: >- Confirmed over-cap reconnect creates or returns a durable `session.display_relaunch` termination operation. - Under the session and unique Stop lock, the Server MUST create at most one display-relaunch operation per session; - a same-owner/device lost-response retry MUST return that same operation. It reuses the existing one-way non-reissued - Stop work/ack path. An existing user Stop in any state MUST win and yield a stable non-202 result; it MUST NOT be reinterpreted as display-relaunch authority. This response is not a manifest, - does not assert termination completion, and does not authorize a replacement session before `applied`. + Before initial operation creation and on every replay, the authenticated principal and active client device/key MUST + match the broker session. Under the session and unique Stop lock, the Server MUST create at most one termination + operation total per broker session. A same-owner/device lost-response retry MUST return that same operation. + If a user Stop wins first, reconnect MUST return a stable non-202 result. If display relaunch wins first, a later user + Stop MUST converge on that same existing `StopOperation` without creating a second operation or issuing a second Terminate. + The operation reuses the existing one-way non-reissued Stop work/ack path. This response is not a manifest, does not + assert termination completion, and does not authorize a replacement session before `applied`. After `applied`, the client submits a fresh `SessionRequest` with a new idempotency key. - Failed or `termination_unconfirmed` outcomes never auto-relaunch. Local Stop or teardown MUST suppress the fresh launch. + Failed or `termination_unconfirmed` outcomes never auto-relaunch. Client local Stop or teardown MUST invalidate + relaunch generation so a later `applied` state cannot cause a fresh launch. Maximum JSON body: 16384 bytes. content: application/json: diff --git a/tools/validate.py b/tools/validate.py index 2beec0e..a9551e7 100644 --- a/tools/validate.py +++ b/tools/validate.py @@ -322,18 +322,27 @@ def main() -> int: reconnect_endpoint = openapi.split(" operationId: reconnectBrokerSession\n", 1)[1].split("\n /api/", 1)[0] assert " '202':\n" in reconnect_endpoint relaunch_response = reconnect_endpoint.split(" '202':\n", 1)[1].split(" '400':", 1)[0] - assert "$defs/StopOperation" in relaunch_response - assert "durable `session.display_relaunch` termination operation" in relaunch_response - assert "MUST create at most one display-relaunch operation per session" in relaunch_response - assert "same-owner/device lost-response retry MUST return that same operation" in relaunch_response - assert "existing user Stop in any state MUST win and yield a stable non-202 result" in relaunch_response - assert "MUST NOT be reinterpreted as display-relaunch authority" in relaunch_response - assert "not a manifest" in relaunch_response - assert "does not assert termination completion" in relaunch_response - assert "does not authorize a replacement session before `applied`" in relaunch_response - assert "fresh `SessionRequest` with a new idempotency key" in relaunch_response - assert "Failed or `termination_unconfirmed` outcomes never auto-relaunch" in relaunch_response - assert "Local Stop or teardown MUST suppress the fresh launch" in relaunch_response + relaunch_schema = relaunch_response.split(" content:\n", 1)[1] + assert relaunch_schema == ( + " application/json:\n" + " schema:\n" + " $ref: ../schemas/control-v1.schema.json#/$defs/StopOperation\n" + ), "reconnect 202 must contain only the exact StopOperation schema reference" + assert "ConnectionManifest" not in relaunch_response + assert "anyOf:" not in relaunch_response and "oneOf:" not in relaunch_response + relaunch_text = " ".join(relaunch_response.split()) + assert "durable `session.display_relaunch` termination operation" in relaunch_text + assert "Before initial operation creation and on every replay, the authenticated principal and active client device/key MUST match the broker session" in relaunch_text + assert "MUST create at most one termination operation total per broker session" in relaunch_text + assert "A same-owner/device lost-response retry MUST return that same operation" in relaunch_text + assert "If a user Stop wins first, reconnect MUST return a stable non-202 result" in relaunch_text + assert "a later user Stop MUST converge on that same existing `StopOperation` without creating a second operation or issuing a second Terminate" in relaunch_text + assert "not a manifest" in relaunch_text + assert "does not assert termination completion" in relaunch_text + assert "does not authorize a replacement session before `applied`" in relaunch_text + assert "fresh `SessionRequest` with a new idempotency key" in relaunch_text + assert "Failed or `termination_unconfirmed` outcomes never auto-relaunch" in relaunch_text + assert "Client local Stop or teardown MUST invalidate relaunch generation so a later `applied` state cannot cause a fresh launch" in relaunch_text for operation_id in ("loginBrowserSession", "rotateNativeCredential", "issueNativeTunnelCredential"): operation = openapi.split(f" operationId: {operation_id}\n", 1)[1].split(" responses:\n", 1)[0] assert "browserCsrf" not in operation, f"{operation_id}: excluded operation gained browser CSRF"