insightfactory-cli 1.1.2.dev31__tar.gz → 1.2.0.dev34__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.
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/CHANGELOG.md +14 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/PKG-INFO +9 -1
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/README.md +8 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/pyproject.toml +1 -1
- insightfactory_cli-1.2.0.dev34/src/if_cli/attribution.py +51 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/constants.py +1 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/http.py +11 -1
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/oauth.py +2 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/router/upstream.py +6 -1
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/conftest.py +2 -0
- insightfactory_cli-1.2.0.dev34/tests/test_attribution.py +160 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/test_router_upstream.py +26 -1
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/uv.lock +1 -1
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/.github/workflows/ci.yml +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/.github/workflows/claude.yml +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/.github/workflows/release.yml +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/.gitignore +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/.python-version +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/AGENTS.md +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/CLAUDE.md +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/LICENSE +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/__init__.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/__main__.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/assets/__init__.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/assets/insightfactoryai-logo.svg +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/cache.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/callback_page.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/cli.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/colour.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/commands/__init__.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/commands/api.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/commands/config.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/commands/login.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/commands/logout.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/commands/mcp.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/commands/profiles.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/commands/set_token.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/commands/token.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/config.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/main.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/router/__init__.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/router/catalog.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/router/content.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/router/log.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/router/policy.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/router/server.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/runtime.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/__init__.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/cache_writer.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/helpers.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/servers.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/test_api.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/test_api_command.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/test_cache.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/test_cli.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/test_config.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/test_config_command.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/test_login.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/test_mcp_command.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/test_oauth.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/test_oauth_flow.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/test_oauth_force_refresh.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/test_profiles.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/test_programmatic_api.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/test_release_tools.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/test_router_catalog.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/test_router_content.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/test_router_log.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/test_router_policy.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/test_router_resources.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/test_router_server.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/test_runtime.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/test_set_token.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/test_token.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tools/check_release.py +0 -0
- {insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tools/release_notes.py +0 -0
|
@@ -11,6 +11,20 @@ that: every PR into `develop` must add an entry here, and a tag whose version ha
|
|
|
11
11
|
section fails rather than releasing an empty page. The version only moves when the
|
|
12
12
|
last one has been released, so a cycle's PRs share a section.
|
|
13
13
|
|
|
14
|
+
## 1.2.0
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- Factory requests carry an `X-IF-UA` header of RFC 9110 product tokens, so the
|
|
19
|
+
Interfaces API can attribute work to the client that made it. The value is
|
|
20
|
+
`if-cli/<version>`, with the contents of `INSIGHTFACTORY_UA` in front when that
|
|
21
|
+
variable holds valid product tokens, for example
|
|
22
|
+
`if-fde-harness/5.10.0 if-cli/1.2.0`. The CLI drops a value that fails the
|
|
23
|
+
grammar or exceeds 256 characters and sends its own token alone. `api`, OAuth
|
|
24
|
+
discovery and the `if-cli mcp` router's calls to each factory send the header.
|
|
25
|
+
Token requests to the identity provider do not, and a header the caller sets
|
|
26
|
+
itself replaces the built one.
|
|
27
|
+
|
|
14
28
|
## 1.1.2
|
|
15
29
|
|
|
16
30
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: insightfactory-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0.dev34
|
|
4
4
|
Summary: Profile-based authentication CLI for the InsightFactory Interfaces API
|
|
5
5
|
Project-URL: Homepage, https://insightfactory.ai
|
|
6
6
|
Author-email: "insightfactory.ai Support" <support@insightfactory.ai>
|
|
@@ -128,6 +128,14 @@ accepted and completed the request after `if-cli` gave up, so re-issuing a
|
|
|
128
128
|
timeout message tells you to confirm the current state before retrying rather
|
|
129
129
|
than repeat the call.
|
|
130
130
|
|
|
131
|
+
Every request to the factory carries an `X-IF-UA` header naming the client,
|
|
132
|
+
`if-cli/<version>` by default. A tool that runs `if-cli` can name itself first
|
|
133
|
+
by setting `INSIGHTFACTORY_UA` to one or more RFC 9110 product tokens, such as
|
|
134
|
+
`if-fde-harness/5.10.0`, and the header becomes
|
|
135
|
+
`if-fde-harness/5.10.0 if-cli/<version>`. The CLI ignores a value that is not
|
|
136
|
+
valid product-token syntax or is longer than 256 characters. Token requests to
|
|
137
|
+
the identity provider never carry the header.
|
|
138
|
+
|
|
131
139
|
Override the config directory with `INSIGHTFACTORY_CONFIG_DIR`, which is useful
|
|
132
140
|
in tests and CI. The default is `~/.insightfactory`.
|
|
133
141
|
|
|
@@ -104,6 +104,14 @@ accepted and completed the request after `if-cli` gave up, so re-issuing a
|
|
|
104
104
|
timeout message tells you to confirm the current state before retrying rather
|
|
105
105
|
than repeat the call.
|
|
106
106
|
|
|
107
|
+
Every request to the factory carries an `X-IF-UA` header naming the client,
|
|
108
|
+
`if-cli/<version>` by default. A tool that runs `if-cli` can name itself first
|
|
109
|
+
by setting `INSIGHTFACTORY_UA` to one or more RFC 9110 product tokens, such as
|
|
110
|
+
`if-fde-harness/5.10.0`, and the header becomes
|
|
111
|
+
`if-fde-harness/5.10.0 if-cli/<version>`. The CLI ignores a value that is not
|
|
112
|
+
valid product-token syntax or is longer than 256 characters. Token requests to
|
|
113
|
+
the identity provider never carry the header.
|
|
114
|
+
|
|
107
115
|
Override the config directory with `INSIGHTFACTORY_CONFIG_DIR`, which is useful
|
|
108
116
|
in tests and CI. The default is `~/.insightfactory`.
|
|
109
117
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""The `X-IF-UA` client-attribution header.
|
|
2
|
+
|
|
3
|
+
The Interfaces API attributes work to the client that made the request by reading
|
|
4
|
+
`X-IF-UA`, a list of RFC 9110 product tokens. It takes the first token, splits it at
|
|
5
|
+
`/` into client and version, and ignores the rest. So a tool that drives `if-cli`
|
|
6
|
+
names itself first through `INSIGHTFACTORY_UA` and `if-cli` appends its own token:
|
|
7
|
+
|
|
8
|
+
X-IF-UA: if-cli/1.2.0 # INSIGHTFACTORY_UA unset
|
|
9
|
+
X-IF-UA: if-fde-harness/5.10.0 if-cli/1.2.0 # INSIGHTFACTORY_UA=if-fde-harness/5.10.0
|
|
10
|
+
|
|
11
|
+
The environment value is only ever sent after it matches the product-token grammar,
|
|
12
|
+
so a stray newline or shell fragment in it cannot add a header or reach the factory.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
19
|
+
from functools import cache
|
|
20
|
+
from importlib.metadata import version
|
|
21
|
+
|
|
22
|
+
from if_cli.constants import ENV_UA
|
|
23
|
+
|
|
24
|
+
HEADER_NAME = "X-IF-UA"
|
|
25
|
+
MAX_CALLER_PRODUCTS_LENGTH = 256
|
|
26
|
+
|
|
27
|
+
# RFC 9110 section 5.6.2: token = 1*tchar. Section 10.1.5: product = token ["/" product-version].
|
|
28
|
+
_TCHAR = r"[!#$%&'*+\-.^_`|~0-9A-Za-z]"
|
|
29
|
+
_PRODUCT = rf"{_TCHAR}+(?:/{_TCHAR}+)?"
|
|
30
|
+
_PRODUCTS = re.compile(rf"{_PRODUCT}(?: {_PRODUCT})*")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@cache
|
|
34
|
+
def _own_product() -> str:
|
|
35
|
+
return f"if-cli/{version('insightfactory-cli')}"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def is_valid_products(value: str) -> bool:
|
|
39
|
+
"""Whether `value` is one or more space-separated product tokens, within the length cap."""
|
|
40
|
+
return len(value) <= MAX_CALLER_PRODUCTS_LENGTH and _PRODUCTS.fullmatch(value) is not None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def build_x_if_ua() -> str:
|
|
44
|
+
"""The `X-IF-UA` value: the caller's products from `INSIGHTFACTORY_UA`, then `if-cli`'s own.
|
|
45
|
+
|
|
46
|
+
An unset, empty or invalid `INSIGHTFACTORY_UA` leaves `if-cli`'s token alone.
|
|
47
|
+
"""
|
|
48
|
+
caller = os.environ.get(ENV_UA)
|
|
49
|
+
if caller and is_valid_products(caller):
|
|
50
|
+
return f"{caller} {_own_product()}"
|
|
51
|
+
return _own_product()
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
ENV_PROFILE: str = "INSIGHTFACTORY_CONFIG_PROFILE"
|
|
2
2
|
ENV_REQUEST_TIMEOUT: str = "INSIGHTFACTORY_REQUEST_TIMEOUT"
|
|
3
|
+
ENV_UA: str = "INSIGHTFACTORY_UA"
|
|
3
4
|
DEFAULT_SCOPE: str = "openid profile email offline_access"
|
|
4
5
|
LOOPBACK_HOST: str = "127.0.0.1"
|
|
5
6
|
DEFAULT_CALLBACK_PORT: int = 53682
|
|
@@ -10,6 +10,7 @@ from email.message import Message
|
|
|
10
10
|
from typing import NoReturn
|
|
11
11
|
from urllib.parse import ParseResult, urljoin, urlparse
|
|
12
12
|
|
|
13
|
+
from if_cli.attribution import HEADER_NAME, build_x_if_ua
|
|
13
14
|
from if_cli.constants import ENV_REQUEST_TIMEOUT
|
|
14
15
|
from if_cli.runtime import die
|
|
15
16
|
|
|
@@ -170,6 +171,7 @@ def fetch_with_timeout(
|
|
|
170
171
|
body: str | bytes | None = None,
|
|
171
172
|
timeout: float | None = None,
|
|
172
173
|
warn_unsafe_retry: bool = False,
|
|
174
|
+
attribute: bool = True,
|
|
173
175
|
) -> HttpResponse:
|
|
174
176
|
"""Issue one request, dying on timeout.
|
|
175
177
|
|
|
@@ -177,7 +179,15 @@ def fetch_with_timeout(
|
|
|
177
179
|
sense for a request the caller issued on the user's behalf against the factory.
|
|
178
180
|
It is wrong advice for an OAuth round trip, where retrying is the correct response
|
|
179
181
|
and there is no factory-side state to confirm.
|
|
182
|
+
|
|
183
|
+
`attribute` adds the `X-IF-UA` header (see `if_cli.attribution`) unless the
|
|
184
|
+
caller already set one. It is on for factory requests and off for the token
|
|
185
|
+
endpoint, which belongs to the identity provider: that is a third party, and the
|
|
186
|
+
header is for the factory's own client metrics.
|
|
180
187
|
"""
|
|
188
|
+
request_headers = dict(headers or {})
|
|
189
|
+
if attribute and not any(name.lower() == HEADER_NAME.lower() for name in request_headers):
|
|
190
|
+
request_headers[HEADER_NAME] = build_x_if_ua()
|
|
181
191
|
data: bytes | None
|
|
182
192
|
if body is None:
|
|
183
193
|
data = None
|
|
@@ -185,7 +195,7 @@ def fetch_with_timeout(
|
|
|
185
195
|
data = body
|
|
186
196
|
else:
|
|
187
197
|
data = body.encode("utf-8")
|
|
188
|
-
request = urllib.request.Request(url, data=data, headers=
|
|
198
|
+
request = urllib.request.Request(url, data=data, headers=request_headers, method=method)
|
|
189
199
|
effective_timeout = resolve_request_timeout(timeout)
|
|
190
200
|
try:
|
|
191
201
|
# Callers pass endpoints validated by parse_http_url or derived from a
|
|
@@ -128,6 +128,8 @@ def _token_request(token_endpoint: str, form: dict[str, str]) -> dict[str, objec
|
|
|
128
128
|
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
129
129
|
body=urlencode(form),
|
|
130
130
|
timeout=DEFAULT_REQUEST_TIMEOUT_S,
|
|
131
|
+
# The token endpoint is the identity provider's, not the factory's.
|
|
132
|
+
attribute=False,
|
|
131
133
|
)
|
|
132
134
|
if not response.ok:
|
|
133
135
|
body = response.text()
|
{insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/router/upstream.py
RENAMED
|
@@ -9,6 +9,7 @@ import mcp_types as types
|
|
|
9
9
|
from mcp.client import Client
|
|
10
10
|
from mcp.client.streamable_http import streamable_http_client
|
|
11
11
|
|
|
12
|
+
from if_cli.attribution import HEADER_NAME, build_x_if_ua
|
|
12
13
|
from if_cli.config import Profile
|
|
13
14
|
from if_cli.http import parse_url, url_origin
|
|
14
15
|
from if_cli.oauth import get_valid_token
|
|
@@ -200,7 +201,11 @@ class Upstream:
|
|
|
200
201
|
async def _run(self, action, *, elicitation_callback=None):
|
|
201
202
|
url = f"{self.profile['host']}/mcp"
|
|
202
203
|
try:
|
|
203
|
-
async with httpx2.AsyncClient(
|
|
204
|
+
async with httpx2.AsyncClient(
|
|
205
|
+
auth=ProfileBearerAuth(self.profile),
|
|
206
|
+
timeout=self.timeout,
|
|
207
|
+
headers={HEADER_NAME: build_x_if_ua()},
|
|
208
|
+
) as http_client:
|
|
204
209
|
async with Client(
|
|
205
210
|
streamable_http_client(url, http_client=http_client),
|
|
206
211
|
elicitation_callback=elicitation_callback,
|
|
@@ -7,5 +7,7 @@ import pytest
|
|
|
7
7
|
def isolate_insightfactory_home(tmp_path, monkeypatch):
|
|
8
8
|
monkeypatch.setenv("INSIGHTFACTORY_CONFIG_DIR", str(tmp_path))
|
|
9
9
|
monkeypatch.delenv("INSIGHTFACTORY_CONFIG_PROFILE", raising=False)
|
|
10
|
+
# The FDE harness sets this in the shells it runs, tests included.
|
|
11
|
+
monkeypatch.delenv("INSIGHTFACTORY_UA", raising=False)
|
|
10
12
|
monkeypatch.setenv("NO_COLOR", "1")
|
|
11
13
|
return tmp_path
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import version
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from if_cli import oauth
|
|
8
|
+
from if_cli.attribution import MAX_CALLER_PRODUCTS_LENGTH, build_x_if_ua
|
|
9
|
+
from if_cli.cache import store_tokens
|
|
10
|
+
from if_cli.config import Profile
|
|
11
|
+
from if_cli.http import fetch_with_timeout
|
|
12
|
+
from tests.servers import Request, Response, ThreadedServer
|
|
13
|
+
|
|
14
|
+
OWN = f"if-cli/{version('insightfactory-cli')}"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _header(headers: dict[str, str], name: str = "X-IF-UA") -> str | None:
|
|
18
|
+
matches = [value for key, value in headers.items() if key.lower() == name.lower()]
|
|
19
|
+
assert len(matches) <= 1, f"{name} sent more than once: {matches}"
|
|
20
|
+
return matches[0] if matches else None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _recording_server(response: Response | None = None) -> tuple[ThreadedServer, list[dict[str, str]]]:
|
|
24
|
+
seen: list[dict[str, str]] = []
|
|
25
|
+
|
|
26
|
+
def handler(request: Request) -> Response:
|
|
27
|
+
seen.append(request.headers)
|
|
28
|
+
return response or Response(body={"ok": True})
|
|
29
|
+
|
|
30
|
+
return ThreadedServer(handler).start(), seen
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _profile(host: str) -> Profile:
|
|
34
|
+
return {
|
|
35
|
+
"name": "a",
|
|
36
|
+
"host": host,
|
|
37
|
+
"audience": f"{host}/api",
|
|
38
|
+
"callback_port": 0,
|
|
39
|
+
"client_id": None,
|
|
40
|
+
"organization": None,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_unset_sends_only_the_cli_token():
|
|
45
|
+
assert build_x_if_ua() == OWN
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@pytest.mark.parametrize(
|
|
49
|
+
"caller",
|
|
50
|
+
[
|
|
51
|
+
"if-fde-harness/5.10.0",
|
|
52
|
+
"if-fde-harness-mcp",
|
|
53
|
+
"if-fde-harness/5.10.0 other/1",
|
|
54
|
+
"a!#$%&'*+-.^_`|~z/9",
|
|
55
|
+
],
|
|
56
|
+
)
|
|
57
|
+
def test_a_valid_value_goes_first(monkeypatch, caller: str):
|
|
58
|
+
monkeypatch.setenv("INSIGHTFACTORY_UA", caller)
|
|
59
|
+
assert build_x_if_ua() == f"{caller} {OWN}"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@pytest.mark.parametrize(
|
|
63
|
+
"caller",
|
|
64
|
+
[
|
|
65
|
+
"",
|
|
66
|
+
" ",
|
|
67
|
+
"\t",
|
|
68
|
+
"if-fde-harness/5.10.0\nX-Injected: 1",
|
|
69
|
+
"if-fde-harness/5.10.0\r\n",
|
|
70
|
+
"if-fde-harness/5.10.0\n",
|
|
71
|
+
"$(whoami)",
|
|
72
|
+
"if-fde-harness/$(whoami)",
|
|
73
|
+
" if-fde-harness/5.10.0",
|
|
74
|
+
"if-fde-harness/5.10.0 ",
|
|
75
|
+
"if-fde-harness/5.10.0 other/1",
|
|
76
|
+
"if-fde-harness/",
|
|
77
|
+
"/5.10.0",
|
|
78
|
+
"a/b/c",
|
|
79
|
+
"if-fde-harness/5.10.0 (comment)",
|
|
80
|
+
"harnessé/1",
|
|
81
|
+
"x" * (MAX_CALLER_PRODUCTS_LENGTH + 1),
|
|
82
|
+
],
|
|
83
|
+
)
|
|
84
|
+
def test_an_invalid_value_is_dropped(monkeypatch, caller: str):
|
|
85
|
+
monkeypatch.setenv("INSIGHTFACTORY_UA", caller)
|
|
86
|
+
assert build_x_if_ua() == OWN
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_a_value_at_the_cap_is_kept(monkeypatch):
|
|
90
|
+
caller = "x" * MAX_CALLER_PRODUCTS_LENGTH
|
|
91
|
+
monkeypatch.setenv("INSIGHTFACTORY_UA", caller)
|
|
92
|
+
assert build_x_if_ua() == f"{caller} {OWN}"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_fetch_with_timeout_sends_the_header(monkeypatch):
|
|
96
|
+
monkeypatch.setenv("INSIGHTFACTORY_UA", "if-fde-harness/5.10.0")
|
|
97
|
+
server, seen = _recording_server()
|
|
98
|
+
try:
|
|
99
|
+
fetch_with_timeout(f"{server.origin}/api/health")
|
|
100
|
+
finally:
|
|
101
|
+
server.close()
|
|
102
|
+
assert _header(seen[0]) == f"if-fde-harness/5.10.0 {OWN}"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_fetch_with_timeout_never_puts_an_invalid_value_on_the_wire(monkeypatch):
|
|
106
|
+
monkeypatch.setenv("INSIGHTFACTORY_UA", "if-fde-harness/5.10.0\r\nX-Injected: 1")
|
|
107
|
+
server, seen = _recording_server()
|
|
108
|
+
try:
|
|
109
|
+
fetch_with_timeout(f"{server.origin}/api/health")
|
|
110
|
+
finally:
|
|
111
|
+
server.close()
|
|
112
|
+
assert _header(seen[0]) == OWN
|
|
113
|
+
assert _header(seen[0], "X-Injected") is None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@pytest.mark.parametrize("name", ["X-IF-UA", "x-if-ua"])
|
|
117
|
+
def test_a_caller_supplied_header_wins(monkeypatch, name: str):
|
|
118
|
+
monkeypatch.setenv("INSIGHTFACTORY_UA", "if-fde-harness/5.10.0")
|
|
119
|
+
server, seen = _recording_server()
|
|
120
|
+
try:
|
|
121
|
+
fetch_with_timeout(f"{server.origin}/api/health", headers={name: "custom/1"})
|
|
122
|
+
finally:
|
|
123
|
+
server.close()
|
|
124
|
+
assert _header(seen[0]) == "custom/1"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_oauth_discovery_on_the_factory_carries_the_header(monkeypatch):
|
|
128
|
+
monkeypatch.setenv("INSIGHTFACTORY_UA", "if-fde-harness/5.10.0")
|
|
129
|
+
server, seen = _recording_server(
|
|
130
|
+
Response(
|
|
131
|
+
body={
|
|
132
|
+
"authorization_endpoint": "https://factory.example/authorize",
|
|
133
|
+
"token_endpoint": "https://factory.example/oauth/token",
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
try:
|
|
138
|
+
oauth._discover(server.origin)
|
|
139
|
+
finally:
|
|
140
|
+
server.close()
|
|
141
|
+
assert _header(seen[0]) == f"if-fde-harness/5.10.0 {OWN}"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_the_identity_provider_token_request_carries_no_header(monkeypatch):
|
|
145
|
+
monkeypatch.setenv("INSIGHTFACTORY_UA", "if-fde-harness/5.10.0")
|
|
146
|
+
idp, seen = _recording_server(Response(body={"access_token": "refreshed-token", "expires_in": 3600}))
|
|
147
|
+
try:
|
|
148
|
+
profile = _profile("https://factory.example")
|
|
149
|
+
store_tokens(
|
|
150
|
+
profile["host"],
|
|
151
|
+
{"access_token": "stale-token", "refresh_token": "refresh-tok", "expires_in": 0},
|
|
152
|
+
f"{idp.origin}/token",
|
|
153
|
+
"test-client",
|
|
154
|
+
replace=True,
|
|
155
|
+
)
|
|
156
|
+
assert oauth.get_valid_token(profile, force_refresh=True) == "refreshed-token"
|
|
157
|
+
finally:
|
|
158
|
+
idp.close()
|
|
159
|
+
assert len(seen) == 1
|
|
160
|
+
assert _header(seen[0]) is None
|
{insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/test_router_upstream.py
RENAMED
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
4
|
import re
|
|
5
|
+
from importlib.metadata import version
|
|
5
6
|
from typing import Any
|
|
6
7
|
|
|
7
8
|
import pytest
|
|
@@ -15,7 +16,7 @@ import mcp_types as types # noqa: E402
|
|
|
15
16
|
from if_cli.cache import store_tokens # noqa: E402
|
|
16
17
|
from if_cli.config import Profile # noqa: E402
|
|
17
18
|
from if_cli.router.upstream import ConnectUrlRecorder, ProfileBearerAuth, Upstream # noqa: E402
|
|
18
|
-
from tests.servers import Response, ThreadedServer # noqa: E402
|
|
19
|
+
from tests.servers import Request, Response, ThreadedServer # noqa: E402
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
def _profile(host: str) -> Profile:
|
|
@@ -85,6 +86,30 @@ def test_auth_remints_the_token_once_on_a_401_and_retries():
|
|
|
85
86
|
idp.close()
|
|
86
87
|
|
|
87
88
|
|
|
89
|
+
def test_the_upstream_client_names_the_router_and_the_cli(monkeypatch):
|
|
90
|
+
"""The router runs with its MCP registration env, so the harness token goes first."""
|
|
91
|
+
monkeypatch.setenv("INSIGHTFACTORY_UA", "if-fde-harness-mcp")
|
|
92
|
+
seen: list[dict[str, str]] = []
|
|
93
|
+
|
|
94
|
+
def handler(request: Request) -> Response:
|
|
95
|
+
seen.append(request.headers)
|
|
96
|
+
# Any error ends the session; only the first request's headers matter.
|
|
97
|
+
return Response(status=500, body="")
|
|
98
|
+
|
|
99
|
+
server = ThreadedServer(handler).start()
|
|
100
|
+
try:
|
|
101
|
+
profile = _profile(server.origin)
|
|
102
|
+
store_tokens(profile["host"], {"access_token": "test-token", "expires_in": 3600}, "", "c", replace=True)
|
|
103
|
+
with pytest.raises(Exception):
|
|
104
|
+
anyio.run(Upstream("prd", profile, timeout=5).list_tools)
|
|
105
|
+
finally:
|
|
106
|
+
server.close()
|
|
107
|
+
assert seen, "the router never reached the factory"
|
|
108
|
+
headers = {name.lower(): value for name, value in seen[0].items()}
|
|
109
|
+
assert headers["x-if-ua"] == f"if-fde-harness-mcp if-cli/{version('insightfactory-cli')}"
|
|
110
|
+
assert headers["authorization"] == "Bearer test-token"
|
|
111
|
+
|
|
112
|
+
|
|
88
113
|
def _url_request(url: str, message: str = "Connect your Databricks account.") -> types.ElicitRequest:
|
|
89
114
|
return types.ElicitRequest(params=types.ElicitRequestURLParams(message=message, url=url))
|
|
90
115
|
|
|
File without changes
|
{insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/.github/workflows/claude.yml
RENAMED
|
File without changes
|
{insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/.github/workflows/release.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/assets/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/callback_page.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/commands/__init__.py
RENAMED
|
File without changes
|
{insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/commands/api.py
RENAMED
|
File without changes
|
{insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/commands/config.py
RENAMED
|
File without changes
|
{insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/commands/login.py
RENAMED
|
File without changes
|
{insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/commands/logout.py
RENAMED
|
File without changes
|
{insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/commands/mcp.py
RENAMED
|
File without changes
|
{insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/commands/profiles.py
RENAMED
|
File without changes
|
{insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/commands/set_token.py
RENAMED
|
File without changes
|
{insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/commands/token.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/router/__init__.py
RENAMED
|
File without changes
|
{insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/router/catalog.py
RENAMED
|
File without changes
|
{insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/router/content.py
RENAMED
|
File without changes
|
|
File without changes
|
{insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/router/policy.py
RENAMED
|
File without changes
|
{insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/src/if_cli/router/server.py
RENAMED
|
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
|
{insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/test_config_command.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/test_oauth_force_refresh.py
RENAMED
|
File without changes
|
|
File without changes
|
{insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/test_programmatic_api.py
RENAMED
|
File without changes
|
{insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/test_release_tools.py
RENAMED
|
File without changes
|
{insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/test_router_catalog.py
RENAMED
|
File without changes
|
{insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/test_router_content.py
RENAMED
|
File without changes
|
|
File without changes
|
{insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/test_router_policy.py
RENAMED
|
File without changes
|
{insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/test_router_resources.py
RENAMED
|
File without changes
|
{insightfactory_cli-1.1.2.dev31 → insightfactory_cli-1.2.0.dev34}/tests/test_router_server.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|