From dc75c23419127a6aacbcc3561d714deb89723fa3 Mon Sep 17 00:00:00 2001 From: Amantux Date: Mon, 20 Jul 2026 16:11:03 +0000 Subject: [PATCH 1/2] Make scalar_http client cookie-session only, not Bearer (i3 auth fidelity) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The real i3 authenticates via the sessionID session cookie; the client was hybrid — it relied on the cookie jar AND also sent Authorization: Bearer using the token the EMULATOR happens to return in the login body. A real appliance does not accept that header, so the Bearer path let the client "work" against the emulator while diverging from the real contract (external review #2). - _headers() no longer adds Authorization; auth rides purely on the cookie jar (login sets sessionID; httpx persists it; 401 still triggers a transparent re-login). The body token is retained for diagnostics only. - New contract test asserts the client sends no Authorization header, the sessionID cookie is present, and a cookie-authenticated read still works — guarding against regressing to the Bearer artifact. 18 scalar_http contract tests green against the in-process emulator. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_012K5Zjc2bggcyJtFbz2D4Pw --- openblade/hardware/scalar_http/session.py | 15 +++++++++------ tests/i3/test_scalar_http_session.py | 13 +++++++++++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/openblade/hardware/scalar_http/session.py b/openblade/hardware/scalar_http/session.py index e126ddf..25c1c87 100644 --- a/openblade/hardware/scalar_http/session.py +++ b/openblade/hardware/scalar_http/session.py @@ -50,8 +50,9 @@ def login(self) -> None: """Establish a session. Raises ``ScalarHttpError`` on failure. The AML login accepts a JSON body and aliases ``username`` to ``name``; the - session cookie lands in the client cookie jar. The bearer token in the body - is captured as a fallback for clients whose jar is not shared across calls. + session cookie lands in the client cookie jar and authenticates every + subsequent request. The emulator also returns a token in the body — retained + for diagnostics only, NOT sent as a Bearer header (see ``_headers``). """ response = self._client.post( self._login_path, @@ -65,10 +66,12 @@ def login(self) -> None: self._logged_in = True def _headers(self) -> dict[str, str]: - headers = dict(_JSON_HEADERS) - if self._token: - headers["Authorization"] = f"Bearer {self._token}" - return headers + # Cookie-session only, to match the real i3: authentication rides on the + # ``sessionID`` cookie persisted in the client's cookie jar from login. We do + # NOT send an ``Authorization: Bearer`` header — that is an emulator-only + # artifact the appliance does not accept, and relying on it would let the + # client "work" against the emulator while diverging from the real contract. + return dict(_JSON_HEADERS) def request( self, diff --git a/tests/i3/test_scalar_http_session.py b/tests/i3/test_scalar_http_session.py index 83835d8..0b491b1 100644 --- a/tests/i3/test_scalar_http_session.py +++ b/tests/i3/test_scalar_http_session.py @@ -63,3 +63,16 @@ def test_scalar_http_session_bad_credentials_raise(emulator_client: TestClient) session.login() assert exc_info.value.status_code == 401 + + +def test_scalar_http_session_authenticates_via_cookie_not_bearer( + emulator_client: TestClient, +) -> None: + # Real i3 fidelity: auth rides on the sessionID cookie, never an Authorization + # header. Guards against regressing to the emulator-only Bearer artifact. + session = _session(emulator_client) + session.login() + + assert "Authorization" not in session._headers() + assert "sessionID" in emulator_client.cookies # cookie jar carries the session + assert session.get_json("/aml/system/status") # cookie-authenticated read works From abe1b0d5b1e460e51814c33f39318a1a4eaefffd Mon Sep 17 00:00:00 2001 From: Amantux Date: Mon, 20 Jul 2026 16:12:12 +0000 Subject: [PATCH 2/2] Run scalar_http client contract tests in backend-tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These in-process (TestClient) real-i3 client tests were not run by any PR CI job — the selective-coverage gap again. Wire them into backend-tests so the client contract (incl. cookie-session auth) is verified at PR time, not only post-merge. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_012K5Zjc2bggcyJtFbz2D4Pw --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f81e57..b9c7168 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,7 @@ jobs: - 'tests/integration/test_sharded_archive_atomicity.py' - 'tests/compat/**' - 'compatibility/**' + - 'tests/i3/test_scalar_http_*.py' - 'pyproject.toml' api: - 'openblade/api/**' @@ -127,6 +128,8 @@ jobs: - run: pytest tests/compat -q --tb=short # Whole safety suite (was only test_safety_regression.py) so the SAFETY_003 # import guard is enforced and cannot silently rot again. + # scalar_http real-i3 client contract tests (in-process via TestClient, no server). + - run: pytest tests/i3/test_scalar_http_session.py tests/i3/test_scalar_http_library_backend.py tests/i3/test_scalar_http_move_medium.py tests/i3/test_scalar_http_wiring.py -q --tb=short - run: pytest tests/safety -q --tb=short api-aml-integration: