1212
1313from __future__ import annotations
1414
15- import json
1615import os
17- import socket
18- import time
16+ import sys
1917
2018import httpx
2119
22- # security-proxy (see ~/src/ali-bot/security-proxy): a localhost credential proxy
23- # that binds a RANDOM port and mints a per-service, daily-rotating gate token,
24- # both handed out over a per-user UNIX socket. Replaces the old fixed
25- # localhost:8888 + static-bearer ccdb-proxy. Every alimonitor.cern.ch artefact is
26- # routed through the "/alimonitor/" route (upstream = alimonitor.cern.ch root), so a
27- # single "alimonitor" gate token covers train-workdir / hyperloop / alihyperloop-data.
28- _AGENT_SOCK = os .path .expanduser (
29- os .environ .get ("SECURITY_PROXY_AGENT_SOCK" , "~/.security-proxy/agent.sock" )
30- )
31- _PROXY_SERVICE = os .environ .get ("SECURITY_PROXY_SERVICE" , "alimonitor" )
32- _creds_cache : dict [str , tuple [int , str , float ]] = {}
20+ # The security-proxy client is shared with the sibling MCP servers; it lives one
21+ # directory up so all of them import the same copy (it used to be duplicated, and
22+ # the copies drifted). See security_proxy_client.__doc__.
23+ sys .path .insert (0 , os .path .dirname (os .path .dirname (os .path .abspath (__file__ ))))
24+ import security_proxy_client as _spc # noqa: E402
3325
34-
35- def _proxy_creds (service : str ) -> tuple [int , str ]:
36- """Return (port, gate_token) for ``service`` from the security-proxy agent socket.
37-
38- Cached ~5 min; the proxy accepts the current and previous token, so a slightly
39- stale cached token still works across the daily rotation. Raises with a clear
40- hint if the proxy isn't running.
41- """
42- now = time .time ()
43- hit = _creds_cache .get (service )
44- if hit and now - hit [2 ] < 300 :
45- return hit [0 ], hit [1 ]
46- try :
47- s = socket .socket (socket .AF_UNIX , socket .SOCK_STREAM )
48- s .settimeout (5.0 )
49- s .connect (_AGENT_SOCK )
50- s .sendall ((service + "\n " ).encode ())
51- buf = b""
52- while not buf .endswith (b"\n " ):
53- chunk = s .recv (4096 )
54- if not chunk :
55- break
56- buf += chunk
57- s .close ()
58- data = json .loads (buf .decode ())
59- except (OSError , ValueError ) as exc :
60- raise RuntimeError (
61- f"security-proxy agent not reachable at { _AGENT_SOCK } ({ exc } ); "
62- "is the proxy running? (see ~/src/ali-bot/security-proxy)"
63- ) from exc
64- if "error" in data :
65- raise RuntimeError (
66- f"security-proxy: { data ['error' ]} ; known services: { data .get ('services' , [])} "
67- )
68- port , token = int (data ["port" ]), data .get ("token" , "" )
69- _creds_cache [service ] = (port , token , now )
70- return port , token
26+ _AGENT_SOCK = _spc .AGENT_SOCK
27+ _PROXY_SERVICE = _spc .DEFAULT_SERVICE
28+ _proxy_creds = _spc .proxy_creds
7129
7230
7331async def fetch_bytes (url : str , proxy_token : str = "" , token : str = "" ) -> bytes :
@@ -76,16 +34,18 @@ async def fetch_bytes(url: str, proxy_token: str = "", token: str = "") -> bytes
7634 ``alimonitor.cern.ch/<path>`` is rewritten to
7735 ``http://127.0.0.1:<port>/alimonitor/<path>``: the random port and a per-service,
7836 daily-rotating gate token come from the security-proxy agent socket
79- (``~/.security-proxy/agent.sock ``; override with ``SECURITY_PROXY_AGENT_SOCK``),
37+ (resolved by ``security_proxy_client ``; override with ``SECURITY_PROXY_AGENT_SOCK``),
8038 and the token is sent as ``Authorization: Bearer``. ``Accept-Encoding: identity``
8139 is required (otherwise the proxy returns a gzip Content-Length mismatch). Retries
8240 transient protocol/read errors up to 3×.
8341
8442 Args:
8543 url: Direct artefact URL, a local path, or a ``file://`` URL.
86- proxy_token: Accepted for backward compatibility but ignored — the gate token
87- is minted from the agent socket.
88- token: Ditto (ignored).
44+ proxy_token: Gate token to use when ``url`` ALREADY points at the security-proxy
45+ (``http://127.0.0.1:<port>/<service>/...``), which carries no
46+ ``alimonitor.cern.ch`` host to trigger the rewrite above. Ignored
47+ for alimonitor URLs, where the token is minted from the agent socket.
48+ token: Fallback for ``proxy_token``.
8949 """
9050 # Local file (a path or a file:// URL) — read directly, no HTTP. Lets a
9151 # locally-generated side-car (igprof-demangle-symbols output) be attached
@@ -103,6 +63,18 @@ async def fetch_bytes(url: str, proxy_token: str = "", token: str = "") -> bytes
10363 fetch_url = f"http://127.0.0.1:{ port } /{ _PROXY_SERVICE } /{ path } "
10464 if gate :
10565 headers ["Authorization" ] = f"Bearer { gate } "
66+ elif url .startswith (("http://127.0.0.1:" , "http://localhost:" )):
67+ # Already a security-proxy URL (pasted from a browser, or built by a caller
68+ # that resolved the random port itself). The rewrite above does not fire, but
69+ # the proxy still demands the gate token — without it every route answers 401.
70+ gate = proxy_token or token
71+ if not gate :
72+ try :
73+ gate = _proxy_creds (_PROXY_SERVICE )[1 ]
74+ except RuntimeError :
75+ gate = ""
76+ if gate :
77+ headers ["Authorization" ] = f"Bearer { gate } "
10678
10779 async with httpx .AsyncClient (verify = False ) as client :
10880 for attempt in range (3 ):
0 commit comments