|
18 | 18 |
|
19 | 19 | from functools import lru_cache |
20 | 20 | from importlib.metadata import PackageNotFoundError |
21 | | -from importlib.metadata import metadata as _pkg_metadata |
22 | 21 | from importlib.metadata import version as _pkg_version |
23 | 22 | from typing import Any, TypeAlias |
24 | 23 |
|
|
37 | 36 | SdkAdvisory: TypeAlias = Error |
38 | 37 |
|
39 | 38 |
|
| 39 | +# Canonical distribution name for the wire ``sdk_id``. Hardcoded (rather |
| 40 | +# than read from ``pyproject.toml``'s ``[project].name``) so installed |
| 41 | +# and dev builds emit the same audit-trail attribution. The |
| 42 | +# ``core/error.json`` dedup contract keys on ``(code, field, sdk_id)``; |
| 43 | +# drift here corrupts multi-hop deduplication. Installed wheels publish |
| 44 | +# the PyPI distribution as ``adcp``; the fully-qualified |
| 45 | +# ``adcontextprotocol-adcp-python`` form is what the spec example uses |
| 46 | +# and what cross-SDK consumers expect. |
| 47 | +_SDK_DIST_NAME: str = "adcontextprotocol-adcp-python" |
| 48 | + |
| 49 | + |
40 | 50 | @lru_cache(maxsize=1) |
41 | 51 | def _resolve_sdk_id() -> str: |
42 | 52 | """Return the wire-format ``sdk_id`` for this SDK build. |
43 | 53 |
|
44 | 54 | Format per ``core/error.json``: ``<sdk_package_name>@<version>``. |
45 | | - Reads the installed distribution's ``Name`` from the package metadata |
46 | | - so the prefix never drifts from the PyPI distribution name — the |
47 | | - audit-trail dedup key depends on it. |
| 55 | + The package-name prefix is fixed (``_SDK_DIST_NAME``) so installed |
| 56 | + and dev builds emit the same attribution; only the version |
| 57 | + component varies between them. Without this, dev installs would |
| 58 | + emit a different ``sdk_id`` from wheel installs and break the |
| 59 | + multi-hop dedup contract for the same SDK. |
48 | 60 |
|
49 | 61 | Cached because the resolution is process-stable: the package |
50 | 62 | metadata doesn't change at runtime. Lazy (computed on first call) |
51 | 63 | so setuptools-scm-style late version resolution still works. |
52 | | - Falls back to a development marker when the package isn't |
53 | | - installed (e.g., running directly out of a checkout without |
54 | | - ``pip install -e``). |
| 64 | + Falls back to a ``0.0.0-dev`` version marker when the package |
| 65 | + isn't installed. |
55 | 66 | """ |
56 | 67 | try: |
57 | | - dist_name = _pkg_metadata("adcp")["Name"] |
58 | 68 | v = _pkg_version("adcp") |
59 | 69 | except PackageNotFoundError: |
60 | | - dist_name = "adcontextprotocol-adcp-python" |
61 | 70 | v = "0.0.0-dev" |
62 | | - return f"{dist_name}@{v}" |
| 71 | + return f"{_SDK_DIST_NAME}@{v}" |
63 | 72 |
|
64 | 73 |
|
65 | 74 | def _echo_identifier(value: str | None) -> str | None: |
66 | | - """Cap seller-controlled identifiers before echoing into advisory details.""" |
| 75 | + """Cap + scrub seller-controlled identifiers before echoing into advisory details. |
| 76 | +
|
| 77 | + Two defenses applied in order: |
| 78 | +
|
| 79 | + 1. **Control-character scrub** — replaces every C0 control char |
| 80 | + (``\\x00``-``\\x1f``), the C1 range (``\\x7f``-``\\x9f``), and |
| 81 | + all Unicode line separators with a literal ``"\\u<hex>"`` |
| 82 | + escape. A seller publishing a ``product_id`` containing ``\\n`` |
| 83 | + or ``\\x1b[`` would otherwise round-trip into |
| 84 | + ``errors[].details.product_id``, forging log lines or |
| 85 | + triggering ANSI escape sequences in operator tooling that |
| 86 | + prints one advisory per line. |
| 87 | +
|
| 88 | + 2. **Length cap** — at 128 chars (after escaping), so a malformed |
| 89 | + seller identifier cannot grow the multi-hop ``errors[]`` array |
| 90 | + unbounded into the idempotency replay cache. |
| 91 | +
|
| 92 | + Returns ``None`` for ``None`` input (the explicit absent-product case). |
| 93 | + """ |
67 | 94 | if value is None: |
68 | 95 | return None |
69 | | - if len(value) <= _MAX_ECHOED_IDENTIFIER_LEN: |
70 | | - return value |
71 | | - return value[:_MAX_ECHOED_IDENTIFIER_LEN] + "…[truncated]" |
| 96 | + scrubbed_chars: list[str] = [] |
| 97 | + for ch in value: |
| 98 | + cp = ord(ch) |
| 99 | + # C0 (incl. \t, \n, \r) + DEL + C1 + LS/PS line separators. |
| 100 | + if cp < 0x20 or 0x7F <= cp <= 0x9F or ch in ("
", "
"): |
| 101 | + scrubbed_chars.append(f"\\u{cp:04x}") |
| 102 | + else: |
| 103 | + scrubbed_chars.append(ch) |
| 104 | + scrubbed = "".join(scrubbed_chars) |
| 105 | + if len(scrubbed) <= _MAX_ECHOED_IDENTIFIER_LEN: |
| 106 | + return scrubbed |
| 107 | + return scrubbed[:_MAX_ECHOED_IDENTIFIER_LEN] + "…[truncated]" |
72 | 108 |
|
73 | 109 |
|
74 | 110 | def __getattr__(name: str) -> Any: |
@@ -124,9 +160,10 @@ def make_sdk_advisory( |
124 | 160 |
|
125 | 161 | # ``SDK_ID`` is resolved lazily via module ``__getattr__`` (above) — listed |
126 | 162 | # here so the public surface is documented + introspectable via ``dir()``. |
| 163 | +# ``_echo_identifier`` and ``_resolve_sdk_id`` are private helpers; not |
| 164 | +# part of ``__all__``. |
127 | 165 | __all__ = [ # noqa: F822 — SDK_ID provided via module __getattr__ |
128 | 166 | "SDK_ID", |
129 | 167 | "SdkAdvisory", |
130 | | - "_echo_identifier", |
131 | 168 | "make_sdk_advisory", |
132 | 169 | ] |
0 commit comments