From 64532393c2e81ffdef3f42e7b78e98c3e8c026f8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 06:17:37 +0900 Subject: [PATCH 1/7] fix: hide provider transport exception details --- scripts/actions_workflow_fleet.py | 10 ++++++++-- tests/quality/test_actions_workflow_fleet.py | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/scripts/actions_workflow_fleet.py b/scripts/actions_workflow_fleet.py index bcafb5ebd..87604f462 100644 --- a/scripts/actions_workflow_fleet.py +++ b/scripts/actions_workflow_fleet.py @@ -160,8 +160,14 @@ def request(self, method: str, path: str) -> HttpResponse: context=ssl.create_default_context(), ) try: - connection.request(method, path, headers=headers) - response = connection.getresponse() + try: + connection.request(method, path, headers=headers) + response = connection.getresponse() + except (OSError, http.client.HTTPException): + raise FleetAuditError( + "upstream_unavailable", + "GitHub API transport failed", + ) from None body = response.read() response_headers = { str(key): str(value) for key, value in response.getheaders() diff --git a/tests/quality/test_actions_workflow_fleet.py b/tests/quality/test_actions_workflow_fleet.py index 07df6450f..ef10928b9 100644 --- a/tests/quality/test_actions_workflow_fleet.py +++ b/tests/quality/test_actions_workflow_fleet.py @@ -818,6 +818,26 @@ def test_https_transport_rejects_relative_paths(self) -> None: transport.request("GET", "repos/o/r") self.assertEqual(raised.exception.reason, "invalid_path") + def test_https_transport_hides_raw_network_errors(self) -> None: + """Provider transport failures use a stable reason without raw exception text.""" + + class _FakeConnection: + def request(self, *args: object, **kwargs: object) -> None: + raise TimeoutError("provider address and token must not escape") + + def close(self) -> None: + return None + + transport = fleet.GithubHttpsTransport("secret-token") + with unittest.mock.patch( + "http.client.HTTPSConnection", return_value=_FakeConnection() + ): + with self.assertRaises(fleet.FleetAuditError) as raised: + transport.request("GET", "/repos/o/r") + self.assertEqual(raised.exception.reason, "upstream_unavailable") + self.assertEqual(raised.exception.message, "GitHub API transport failed") + self.assertNotIn("provider address", str(raised.exception)) + def test_build_transport_uses_standard_token(self) -> None: """build_transport reads GITHUB_TOKEN and does not invent a TEPP PAT name.""" From ad5944a9d759f6012bd328793692e571004d8f70 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 06:17:37 +0900 Subject: [PATCH 2/7] fix: hide provider transport exception details --- scripts/actions_workflow_fleet.py | 8 ++++---- tests/quality/test_actions_workflow_fleet.py | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/scripts/actions_workflow_fleet.py b/scripts/actions_workflow_fleet.py index 87604f462..4d887dfa1 100644 --- a/scripts/actions_workflow_fleet.py +++ b/scripts/actions_workflow_fleet.py @@ -163,15 +163,15 @@ def request(self, method: str, path: str) -> HttpResponse: try: connection.request(method, path, headers=headers) response = connection.getresponse() + body = response.read() + response_headers = { + str(key): str(value) for key, value in response.getheaders() + } except (OSError, http.client.HTTPException): raise FleetAuditError( "upstream_unavailable", "GitHub API transport failed", ) from None - body = response.read() - response_headers = { - str(key): str(value) for key, value in response.getheaders() - } return HttpResponse( status=int(response.status), headers=response_headers, diff --git a/tests/quality/test_actions_workflow_fleet.py b/tests/quality/test_actions_workflow_fleet.py index ef10928b9..e4d8c6df6 100644 --- a/tests/quality/test_actions_workflow_fleet.py +++ b/tests/quality/test_actions_workflow_fleet.py @@ -821,9 +821,19 @@ def test_https_transport_rejects_relative_paths(self) -> None: def test_https_transport_hides_raw_network_errors(self) -> None: """Provider transport failures use a stable reason without raw exception text.""" + class _FakeResponse: + def read(self) -> bytes: + raise TimeoutError("provider body and token must not escape") + + def getheaders(self) -> list[tuple[str, str]]: + return [] + class _FakeConnection: def request(self, *args: object, **kwargs: object) -> None: - raise TimeoutError("provider address and token must not escape") + return None + + def getresponse(self) -> _FakeResponse: + return _FakeResponse() def close(self) -> None: return None @@ -836,7 +846,7 @@ def close(self) -> None: transport.request("GET", "/repos/o/r") self.assertEqual(raised.exception.reason, "upstream_unavailable") self.assertEqual(raised.exception.message, "GitHub API transport failed") - self.assertNotIn("provider address", str(raised.exception)) + self.assertNotIn("provider body", str(raised.exception)) def test_build_transport_uses_standard_token(self) -> None: """build_transport reads GITHUB_TOKEN and does not invent a TEPP PAT name.""" From 31066fcffc7a7355f0bf7e6b2f8f864ce32598ba Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 22:13:08 +0900 Subject: [PATCH 3/7] fix(api): refuse x-apikey credential header aliases The naruon interchange treated only x-api-key as a secret header, so x-apikey and x-api_key were admitted. Drive refuse_credential_headers with those aliases and deny them as AuthorizationDenied. --- CHANGELOG.md | 1 + crates/tepp_api/src/naruon_http.rs | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36c2e8dd9..fef3a205f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,6 +81,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ### Security +- Naruon interchange refuses `x-apikey` / `x-api_key` credential-header aliases, not only `x-api-key`. - Disabled-state classification and live disable confirmation now accept GitHub's official `disabled_manually`, `disabled_fork`, `disabled_inactivity`, and `deleted` registry states so orphan bootstrap/repair identities can be retired without name-only heuristics. - Prohibited `COPILOT_GITHUB_TOKEN` and reserved `NVIDIA_NIM_API_KEY` for approved LLM test and development workflows. - Defined purpose-bound PII access, opaque analytical identifiers, separately protected identity mapping, selective model-provider disclosure, retention/deletion, and privileged audit controls instead of destructive blanket masking. diff --git a/crates/tepp_api/src/naruon_http.rs b/crates/tepp_api/src/naruon_http.rs index 2d1a6c2fc..1e12f76af 100644 --- a/crates/tepp_api/src/naruon_http.rs +++ b/crates/tepp_api/src/naruon_http.rs @@ -155,6 +155,9 @@ pub(crate) fn header_is_credential(name: &str) -> bool { || lowered == "proxy-authorization" || lowered == "cookie" || lowered == "x-api-key" + || lowered.contains("api-key") + || lowered.contains("api_key") + || lowered.contains("apikey") || lowered.contains("token") || lowered.contains("copilot") || lowered.contains("github") @@ -296,6 +299,13 @@ mod tests { refuse_credential_headers(&[("x-api-key", "k")]), Err(ApiError::AuthorizationDenied) ); + for name in ["x-apikey", "x-api_key", "X-ApiKey"] { + assert_eq!( + refuse_credential_headers(&[(name, "k")]), + Err(ApiError::AuthorizationDenied), + "header={name}" + ); + } assert_eq!( refuse_credential_headers(&[("x-github-token", "t")]), Err(ApiError::AuthorizationDenied) From 8e4d70a97bc1e152adb10d1b680a5ca4e2912f23 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 22:18:56 +0900 Subject: [PATCH 4/7] test(api): refuse x-apikey aliases on the live naruon listener Drive NaruonLiveService::handle_http with x-apikey, x-api_key, and X-ApiKey so the live header gate, not only the interchange helper, denies those credential aliases. --- crates/tepp_api/tests/naruon_live_http_contract.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/tepp_api/tests/naruon_live_http_contract.rs b/crates/tepp_api/tests/naruon_live_http_contract.rs index 485f3f506..86493f562 100644 --- a/crates/tepp_api/tests/naruon_live_http_contract.rs +++ b/crates/tepp_api/tests/naruon_live_http_contract.rs @@ -358,6 +358,9 @@ fn handle_http_refuses_credential_headers_and_reserved_overrides() { ), ("cookie", "a=b", 403, "authorization_denied"), ("x-api-key", "k", 403, "authorization_denied"), + ("x-apikey", "k", 403, "authorization_denied"), + ("x-api_key", "k", 403, "authorization_denied"), + ("X-ApiKey", "k", 403, "authorization_denied"), ("x-github-token", "t", 403, "authorization_denied"), ("x-copilot-session", "s", 403, "authorization_denied"), ( From fa169e8e41f13d3bae6aff242db68ebf8281e112 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 22:49:59 +0900 Subject: [PATCH 5/7] fix(transport): map close OSError and hyphenated api-key aliases Cover x-vendor-api-key so the naruon contains(api-key) branch is exercised. Convert HTTPSConnection.close() OSError to the stable upstream_unavailable reason without replacing an already-mapped request or response failure, and expand transport tests across request, getresponse, getheaders, read, and close stages. --- CHANGELOG.md | 3 +- crates/tepp_api/src/naruon_http.rs | 2 +- .../tests/naruon_live_http_contract.rs | 1 + scripts/actions_workflow_fleet.py | 15 ++- tests/quality/test_actions_workflow_fleet.py | 107 ++++++++++++++++-- 5 files changed, 111 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fef3a205f..a00f96191 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,7 +81,8 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ### Security -- Naruon interchange refuses `x-apikey` / `x-api_key` credential-header aliases, not only `x-api-key`. +- Naruon interchange refuses `x-apikey`, `x-api_key`, and hyphenated `api-key` credential-header aliases, not only `x-api-key`. +- GitHub HTTPS fleet transport maps request, response, and close-path network exceptions to `upstream_unavailable` without leaking raw provider exception text. - Disabled-state classification and live disable confirmation now accept GitHub's official `disabled_manually`, `disabled_fork`, `disabled_inactivity`, and `deleted` registry states so orphan bootstrap/repair identities can be retired without name-only heuristics. - Prohibited `COPILOT_GITHUB_TOKEN` and reserved `NVIDIA_NIM_API_KEY` for approved LLM test and development workflows. - Defined purpose-bound PII access, opaque analytical identifiers, separately protected identity mapping, selective model-provider disclosure, retention/deletion, and privileged audit controls instead of destructive blanket masking. diff --git a/crates/tepp_api/src/naruon_http.rs b/crates/tepp_api/src/naruon_http.rs index 1e12f76af..2c4770dc0 100644 --- a/crates/tepp_api/src/naruon_http.rs +++ b/crates/tepp_api/src/naruon_http.rs @@ -299,7 +299,7 @@ mod tests { refuse_credential_headers(&[("x-api-key", "k")]), Err(ApiError::AuthorizationDenied) ); - for name in ["x-apikey", "x-api_key", "X-ApiKey"] { + for name in ["x-apikey", "x-api_key", "X-ApiKey", "x-vendor-api-key"] { assert_eq!( refuse_credential_headers(&[(name, "k")]), Err(ApiError::AuthorizationDenied), diff --git a/crates/tepp_api/tests/naruon_live_http_contract.rs b/crates/tepp_api/tests/naruon_live_http_contract.rs index 86493f562..dfb73c56f 100644 --- a/crates/tepp_api/tests/naruon_live_http_contract.rs +++ b/crates/tepp_api/tests/naruon_live_http_contract.rs @@ -361,6 +361,7 @@ fn handle_http_refuses_credential_headers_and_reserved_overrides() { ("x-apikey", "k", 403, "authorization_denied"), ("x-api_key", "k", 403, "authorization_denied"), ("X-ApiKey", "k", 403, "authorization_denied"), + ("x-vendor-api-key", "k", 403, "authorization_denied"), ("x-github-token", "t", 403, "authorization_denied"), ("x-copilot-session", "s", 403, "authorization_denied"), ( diff --git a/scripts/actions_workflow_fleet.py b/scripts/actions_workflow_fleet.py index 4d887dfa1..6b27a520e 100644 --- a/scripts/actions_workflow_fleet.py +++ b/scripts/actions_workflow_fleet.py @@ -159,6 +159,7 @@ def request(self, method: str, path: str) -> HttpResponse: timeout=60, context=ssl.create_default_context(), ) + request_error: FleetAuditError | None = None try: try: connection.request(method, path, headers=headers) @@ -168,17 +169,25 @@ def request(self, method: str, path: str) -> HttpResponse: str(key): str(value) for key, value in response.getheaders() } except (OSError, http.client.HTTPException): - raise FleetAuditError( + request_error = FleetAuditError( "upstream_unavailable", "GitHub API transport failed", - ) from None + ) + raise request_error from None return HttpResponse( status=int(response.status), headers=response_headers, body=body, ) finally: - connection.close() + try: + connection.close() + except OSError: + if request_error is None: + raise FleetAuditError( + "upstream_unavailable", + "GitHub API transport failed", + ) from None def _utc_now() -> str: diff --git a/tests/quality/test_actions_workflow_fleet.py b/tests/quality/test_actions_workflow_fleet.py index e4d8c6df6..fa6f35fe6 100644 --- a/tests/quality/test_actions_workflow_fleet.py +++ b/tests/quality/test_actions_workflow_fleet.py @@ -2,6 +2,7 @@ from __future__ import annotations +import http.client import io import json import os @@ -821,14 +822,81 @@ def test_https_transport_rejects_relative_paths(self) -> None: def test_https_transport_hides_raw_network_errors(self) -> None: """Provider transport failures use a stable reason without raw exception text.""" + cases: tuple[tuple[str, BaseException], ...] = ( + ("request", OSError("provider request and token must not escape")), + ( + "request", + http.client.HTTPException("provider request http must not escape"), + ), + ("getresponse", OSError("provider response and token must not escape")), + ( + "getresponse", + http.client.HTTPException("provider response http must not escape"), + ), + ("getheaders", OSError("provider headers and token must not escape")), + ( + "getheaders", + http.client.HTTPException("provider headers http must not escape"), + ), + ("read", TimeoutError("provider body and token must not escape")), + ("read", http.client.IncompleteRead(b"partial")), + ) + for stage, error in cases: + with self.subTest(stage=stage, error_type=type(error).__name__): + closed = {"called": False} + + class _FakeResponse: + def read(self) -> bytes: + if stage == "read": + raise error + return b'{"ok":true}' + + def getheaders(self) -> list[tuple[str, str]]: + if stage == "getheaders": + raise error + return [] + + class _FakeConnection: + def request(self, *args: object, **kwargs: object) -> None: + if stage == "request": + raise error + + def getresponse(self) -> _FakeResponse: + if stage == "getresponse": + raise error + return _FakeResponse() + + def close(self) -> None: + closed["called"] = True + + transport = fleet.GithubHttpsTransport("secret-token") + with unittest.mock.patch( + "http.client.HTTPSConnection", return_value=_FakeConnection() + ): + with self.assertRaises(fleet.FleetAuditError) as raised: + transport.request("GET", "/repos/o/r") + self.assertEqual(raised.exception.reason, "upstream_unavailable") + self.assertEqual( + raised.exception.message, "GitHub API transport failed" + ) + self.assertNotIn(str(error), str(raised.exception)) + self.assertTrue(closed["called"]) + + def test_https_transport_hides_close_errors_without_overwriting_request_errors( + self, + ) -> None: + """Close-path OSError maps to the stable reason and cannot replace it.""" + class _FakeResponse: + status = 200 + def read(self) -> bytes: - raise TimeoutError("provider body and token must not escape") + return b'{"ok":true}' def getheaders(self) -> list[tuple[str, str]]: - return [] + return [("Content-Type", "application/json")] - class _FakeConnection: + class _CloseFails: def request(self, *args: object, **kwargs: object) -> None: return None @@ -836,17 +904,32 @@ def getresponse(self) -> _FakeResponse: return _FakeResponse() def close(self) -> None: - return None + raise OSError("provider close and token must not escape") + + class _RequestAndCloseFail: + def request(self, *args: object, **kwargs: object) -> None: + raise OSError("provider request and token must not escape") + + def getresponse(self) -> _FakeResponse: + raise AssertionError("getresponse must not run after request failure") + + def close(self) -> None: + raise OSError("provider close and token must not escape") transport = fleet.GithubHttpsTransport("secret-token") - with unittest.mock.patch( - "http.client.HTTPSConnection", return_value=_FakeConnection() - ): - with self.assertRaises(fleet.FleetAuditError) as raised: - transport.request("GET", "/repos/o/r") - self.assertEqual(raised.exception.reason, "upstream_unavailable") - self.assertEqual(raised.exception.message, "GitHub API transport failed") - self.assertNotIn("provider body", str(raised.exception)) + for connection in (_CloseFails(), _RequestAndCloseFail()): + with self.subTest(connection=type(connection).__name__): + with unittest.mock.patch( + "http.client.HTTPSConnection", return_value=connection + ): + with self.assertRaises(fleet.FleetAuditError) as raised: + transport.request("GET", "/repos/o/r") + self.assertEqual(raised.exception.reason, "upstream_unavailable") + self.assertEqual( + raised.exception.message, "GitHub API transport failed" + ) + self.assertNotIn("provider close", str(raised.exception)) + self.assertNotIn("provider request", str(raised.exception)) def test_build_transport_uses_standard_token(self) -> None: """build_transport reads GITHUB_TOKEN and does not invent a TEPP PAT name.""" From 60e936b43740c238d9a4083b529a3d0a12265c12 Mon Sep 17 00:00:00 2001 From: opencode-agent Date: Mon, 24 Aug 2026 12:34:57 +0900 Subject: [PATCH 6/7] fix(transport): drop subsumed api-key literal; mask close-path HTTPException - Remove the literal x-api-key arm fully covered by the api-key substring check so the credential-header predicate stays single-source. - Catch http.client.HTTPException alongside OSError when closing the transport connection so close failures cannot leak raw provider text. --- crates/tepp_api/src/naruon_http.rs | 1 - scripts/actions_workflow_fleet.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/tepp_api/src/naruon_http.rs b/crates/tepp_api/src/naruon_http.rs index 2c4770dc0..5f4c5ffa5 100644 --- a/crates/tepp_api/src/naruon_http.rs +++ b/crates/tepp_api/src/naruon_http.rs @@ -154,7 +154,6 @@ pub(crate) fn header_is_credential(name: &str) -> bool { lowered == "authorization" || lowered == "proxy-authorization" || lowered == "cookie" - || lowered == "x-api-key" || lowered.contains("api-key") || lowered.contains("api_key") || lowered.contains("apikey") diff --git a/scripts/actions_workflow_fleet.py b/scripts/actions_workflow_fleet.py index 6b27a520e..fc2782d23 100644 --- a/scripts/actions_workflow_fleet.py +++ b/scripts/actions_workflow_fleet.py @@ -182,7 +182,7 @@ def request(self, method: str, path: str) -> HttpResponse: finally: try: connection.close() - except OSError: + except (OSError, http.client.HTTPException): if request_error is None: raise FleetAuditError( "upstream_unavailable", From 54c0a4607e373a603eeba54fec5e9e60097db634 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 15:15:02 +0900 Subject: [PATCH 7/7] test(quality): document transport error doubles --- tests/quality/test_actions_workflow_fleet.py | 56 ++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/tests/quality/test_actions_workflow_fleet.py b/tests/quality/test_actions_workflow_fleet.py index fa6f35fe6..9255fe18e 100644 --- a/tests/quality/test_actions_workflow_fleet.py +++ b/tests/quality/test_actions_workflow_fleet.py @@ -216,6 +216,8 @@ def test_permission_not_found_and_upstream_errors_are_distinct(self) -> None: """403, 404, and 5xx stay distinct so operators cannot treat them as empty.""" def expect(status: int, reason: str) -> None: + """Assert one HTTP status maps to its stable fleet-audit reason.""" + transport = FakeTransport( { ( @@ -758,19 +760,29 @@ class _FakeResponse: status = 403 def read(self) -> bytes: + """Return the body used by the status-mapping response.""" + return b'{"message":"no"}' def getheaders(self) -> list[tuple[str, str]]: + """Return the headers used by the status-mapping response.""" + return [("X-GitHub", "yes")] class _FakeConnection: def request(self, *args: object, **kwargs: object) -> None: + """Accept the request without changing the fake response.""" + return None def getresponse(self) -> _FakeResponse: + """Return the status-mapping fake response.""" + return _FakeResponse() def close(self) -> None: + """Close the status-mapping fake connection.""" + return None transport = fleet.GithubHttpsTransport("secret-token") @@ -788,19 +800,29 @@ class _FakeResponse: status = 200 def read(self) -> bytes: + """Return the body used by the normal response test.""" + return b'{"ok":true}' def getheaders(self) -> list[tuple[str, str]]: + """Return the headers used by the normal response test.""" + return [("Content-Type", "application/json")] class _FakeConnection: def request(self, *args: object, **kwargs: object) -> None: + """Accept the request without changing the fake response.""" + return None def getresponse(self) -> _FakeResponse: + """Return the normal fake response.""" + return _FakeResponse() def close(self) -> None: + """Close the normal fake connection.""" + return None transport = fleet.GithubHttpsTransport("secret-token") @@ -847,26 +869,36 @@ def test_https_transport_hides_raw_network_errors(self) -> None: class _FakeResponse: def read(self) -> bytes: + """Raise the stage-specific body error or return valid JSON.""" + if stage == "read": raise error return b'{"ok":true}' def getheaders(self) -> list[tuple[str, str]]: + """Raise the stage-specific header error or return no headers.""" + if stage == "getheaders": raise error return [] class _FakeConnection: def request(self, *args: object, **kwargs: object) -> None: + """Raise the stage-specific request error when selected.""" + if stage == "request": raise error def getresponse(self) -> _FakeResponse: + """Raise the stage-specific response error or return a response.""" + if stage == "getresponse": raise error return _FakeResponse() def close(self) -> None: + """Record that the transport always closes the connection.""" + closed["called"] = True transport = fleet.GithubHttpsTransport("secret-token") @@ -891,29 +923,45 @@ class _FakeResponse: status = 200 def read(self) -> bytes: + """Return a valid response body for the close-path test.""" + return b'{"ok":true}' def getheaders(self) -> list[tuple[str, str]]: + """Return a valid response header for the close-path test.""" + return [("Content-Type", "application/json")] class _CloseFails: def request(self, *args: object, **kwargs: object) -> None: + """Complete the request before close raises the transport error.""" + return None def getresponse(self) -> _FakeResponse: + """Return a valid response before close raises the transport error.""" + return _FakeResponse() def close(self) -> None: + """Raise a close error whose provider text must be redacted.""" + raise OSError("provider close and token must not escape") class _RequestAndCloseFail: def request(self, *args: object, **kwargs: object) -> None: + """Raise the request error before the close error also occurs.""" + raise OSError("provider request and token must not escape") def getresponse(self) -> _FakeResponse: + """Fail the test if transport continues after request failure.""" + raise AssertionError("getresponse must not run after request failure") def close(self) -> None: + """Raise a close error without replacing the request failure.""" + raise OSError("provider close and token must not escape") transport = fleet.GithubHttpsTransport("secret-token") @@ -1116,10 +1164,14 @@ class _AlternatingWorkflowTransport(FakeTransport): """Return active then disabled for the same workflow GET during apply.""" def __init__(self, responses: dict[tuple[str, str], fleet.HttpResponse]) -> None: + """Store responses and initialize per-path observation counts.""" + super().__init__(responses) self._get_counts: dict[str, int] = {} def request(self, method: str, path: str) -> fleet.HttpResponse: + """Return active then disabled state for each workflow confirmation.""" + if method == "GET" and path.endswith("/actions/workflows/10"): count = self._get_counts.get(path, 0) self._get_counts[path] = count + 1 @@ -1145,10 +1197,14 @@ class _ConfirmFailsTransport(FakeTransport): """Leave workflow 10 active after a successful disable PUT.""" def __init__(self, responses: dict[tuple[str, str], fleet.HttpResponse]) -> None: + """Store responses and initialize the confirmation read counter.""" + super().__init__(responses) self._gets = 0 def request(self, method: str, path: str) -> fleet.HttpResponse: + """Keep workflow 10 active so post-disable confirmation fails closed.""" + if method == "GET" and path.endswith("/actions/workflows/10"): self.calls.append((method, path)) self._gets += 1