hypercli-sdk 2026.4.21__tar.gz → 2026.4.22__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/PKG-INFO +1 -1
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/hypercli/__init__.py +1 -1
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/hypercli/openclaw/gateway.py +20 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/pyproject.toml +1 -1
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/tests/test_gateway.py +87 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/.gitignore +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/README.md +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/hypercli/agent.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/hypercli/agents.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/hypercli/billing.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/hypercli/client.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/hypercli/config.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/hypercli/files.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/hypercli/gateway.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/hypercli/http.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/hypercli/instances.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/hypercli/job/__init__.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/hypercli/job/base.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/hypercli/job/comfyui.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/hypercli/job/gradio.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/hypercli/jobs.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/hypercli/keys.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/hypercli/logs.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/hypercli/models.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/hypercli/openclaw/__init__.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/hypercli/renders.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/hypercli/shell.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/hypercli/user.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/hypercli/voice.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/hypercli/x402.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/tests/integration/conftest.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/tests/integration/test_agents.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/tests/integration/test_auth.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/tests/integration/test_billing.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/tests/integration/test_instances.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/tests/integration/test_jobs_dryrun.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/tests/integration/test_keys.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/tests/integration/test_renders.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/tests/test_agents.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/tests/test_apply_params.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/tests/test_bootstrap_console_test_key.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/tests/test_bootstrap_dev_test_keys.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/tests/test_claw.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/tests/test_config.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/tests/test_exec_shell_dryrun.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/tests/test_gateway_retry.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/tests/test_graph_to_api.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/tests/test_http.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/tests/test_jobs.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/tests/test_keys.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/tests/test_models.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/tests/test_renders_subscription.py +0 -0
- {hypercli_sdk-2026.4.21 → hypercli_sdk-2026.4.22}/tests/test_voice.py +0 -0
|
@@ -300,6 +300,10 @@ def _read_connect_pairing_request_id(details: Any) -> str | None:
|
|
|
300
300
|
return request_id.strip() if isinstance(request_id, str) and request_id.strip() else None
|
|
301
301
|
|
|
302
302
|
|
|
303
|
+
def _is_concurrent_pairing_approval_race(exc: Exception) -> bool:
|
|
304
|
+
return "unknown requestid" in str(exc).lower()
|
|
305
|
+
|
|
306
|
+
|
|
303
307
|
def _is_retryable_connect_error(exc: Exception) -> bool:
|
|
304
308
|
status_code = getattr(exc, "status_code", None)
|
|
305
309
|
response = getattr(exc, "response", None)
|
|
@@ -1081,6 +1085,22 @@ class GatewayClient:
|
|
|
1081
1085
|
delay = min(delay * BACKOFF_MULTIPLIER, MAX_RECONNECT_DELAY)
|
|
1082
1086
|
continue
|
|
1083
1087
|
except Exception as approval_error:
|
|
1088
|
+
if _is_concurrent_pairing_approval_race(approval_error):
|
|
1089
|
+
self._update_pairing_state(
|
|
1090
|
+
GatewayPairingState(
|
|
1091
|
+
request_id=request_id,
|
|
1092
|
+
role=OPERATOR_ROLE,
|
|
1093
|
+
gateway_url=self.url,
|
|
1094
|
+
device_id=identity.device_id,
|
|
1095
|
+
status="approved",
|
|
1096
|
+
updated_at_ms=_now_ms(),
|
|
1097
|
+
)
|
|
1098
|
+
)
|
|
1099
|
+
if ws is not None:
|
|
1100
|
+
await ws.close()
|
|
1101
|
+
await asyncio.sleep(delay)
|
|
1102
|
+
delay = min(delay * BACKOFF_MULTIPLIER, MAX_RECONNECT_DELAY)
|
|
1103
|
+
continue
|
|
1084
1104
|
self._update_pairing_state(
|
|
1085
1105
|
GatewayPairingState(
|
|
1086
1106
|
request_id=request_id,
|
|
@@ -122,6 +122,93 @@ async def test_connect_auto_approves_pairing_and_reconnects(monkeypatch: pytest.
|
|
|
122
122
|
await client.close()
|
|
123
123
|
|
|
124
124
|
|
|
125
|
+
@pytest.mark.asyncio
|
|
126
|
+
async def test_connect_treats_unknown_request_id_as_concurrent_pairing_approval(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
127
|
+
sockets: list[MockConnection] = []
|
|
128
|
+
|
|
129
|
+
async def fake_connect(*args, **kwargs):
|
|
130
|
+
conn = MockConnection()
|
|
131
|
+
sockets.append(conn)
|
|
132
|
+
return conn
|
|
133
|
+
|
|
134
|
+
approvals: list[tuple[str, str]] = []
|
|
135
|
+
|
|
136
|
+
async def fake_approve(self: GatewayClient, request_id: str) -> None:
|
|
137
|
+
approvals.append((self.deployment_id or "", request_id))
|
|
138
|
+
raise RuntimeError("unknown requestId")
|
|
139
|
+
|
|
140
|
+
monkeypatch.setattr("hypercli.openclaw.gateway.websockets.connect", fake_connect)
|
|
141
|
+
monkeypatch.setattr(GatewayClient, "_approve_pairing_request", fake_approve)
|
|
142
|
+
|
|
143
|
+
client = GatewayClient(
|
|
144
|
+
url="wss://openclaw-agent.example",
|
|
145
|
+
token="jwt-token",
|
|
146
|
+
gateway_token="gw-token",
|
|
147
|
+
deployment_id="deployment-123",
|
|
148
|
+
api_key="agent-key",
|
|
149
|
+
api_base="https://api.dev.hypercli.com/agents",
|
|
150
|
+
auto_approve_pairing=True,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
connect_task = asyncio.create_task(client.connect())
|
|
154
|
+
while not sockets:
|
|
155
|
+
await asyncio.sleep(0)
|
|
156
|
+
|
|
157
|
+
first = sockets[0]
|
|
158
|
+
first.push({"type": "event", "event": "connect.challenge", "payload": {"nonce": "nonce-1"}})
|
|
159
|
+
while not first.sent:
|
|
160
|
+
await asyncio.sleep(0)
|
|
161
|
+
connect_request = first.sent[0]
|
|
162
|
+
first.push(
|
|
163
|
+
{
|
|
164
|
+
"type": "res",
|
|
165
|
+
"id": connect_request["id"],
|
|
166
|
+
"ok": False,
|
|
167
|
+
"error": {
|
|
168
|
+
"code": "INVALID_REQUEST",
|
|
169
|
+
"message": "pairing required",
|
|
170
|
+
"details": {
|
|
171
|
+
"code": "PAIRING_REQUIRED",
|
|
172
|
+
"requestId": "pairing-req-race",
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
}
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
while len(sockets) < 2:
|
|
179
|
+
await asyncio.sleep(0.05)
|
|
180
|
+
|
|
181
|
+
second = sockets[1]
|
|
182
|
+
second.push({"type": "event", "event": "connect.challenge", "payload": {"nonce": "nonce-2"}})
|
|
183
|
+
while not second.sent:
|
|
184
|
+
await asyncio.sleep(0)
|
|
185
|
+
reconnect_request = second.sent[0]
|
|
186
|
+
second.push(
|
|
187
|
+
{
|
|
188
|
+
"type": "res",
|
|
189
|
+
"id": reconnect_request["id"],
|
|
190
|
+
"ok": True,
|
|
191
|
+
"payload": {
|
|
192
|
+
"protocol": 3,
|
|
193
|
+
"server": {"version": "test"},
|
|
194
|
+
"auth": {
|
|
195
|
+
"deviceToken": "device-token-race",
|
|
196
|
+
"role": "operator",
|
|
197
|
+
"scopes": ["operator.admin"],
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
}
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
await connect_task
|
|
204
|
+
|
|
205
|
+
assert approvals == [("deployment-123", "pairing-req-race")]
|
|
206
|
+
assert client.is_connected is True
|
|
207
|
+
assert client.pending_pairing is None
|
|
208
|
+
|
|
209
|
+
await client.close()
|
|
210
|
+
|
|
211
|
+
|
|
125
212
|
@pytest.mark.asyncio
|
|
126
213
|
async def test_approve_pairing_request_uses_direct_local_pairing_api(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
127
214
|
captured: dict = {}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|