cygnos-capture-proxy 0.1.0__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.
@@ -0,0 +1,88 @@
1
+ Cygnos Proprietary Software License
2
+
3
+ Copyright (c) 2026 Cygnos AI Private Limited. All rights reserved.
4
+
5
+ 1. DEFINITIONS
6
+
7
+ "Software" means the Cygnos client software distributed in this package,
8
+ including its source code, object code, and accompanying documentation.
9
+
10
+ "Licensor" means Cygnos AI Private Limited, a company incorporated under
11
+ the laws of India with its registered office at Jaipur, Rajasthan, India.
12
+
13
+ "Service" means the Cygnos hosted service operated by Licensor.
14
+
15
+ "You" means the individual or legal entity exercising rights under this
16
+ License.
17
+
18
+ 2. GRANT OF LICENSE
19
+
20
+ Subject to Your compliance with this License and with a current written
21
+ agreement with Licensor governing use of the Service, Licensor grants You a
22
+ non-exclusive, non-transferable, non-sublicensable, revocable license to
23
+ install and use the Software solely to transmit telemetry from Your systems
24
+ to the Service, and for no other purpose.
25
+
26
+ 3. RESTRICTIONS
27
+
28
+ You may not:
29
+
30
+ (a) copy the Software except as reasonably necessary for installation and
31
+ backup;
32
+ (b) modify, adapt, translate, or create derivative works of the Software;
33
+ (c) reverse engineer, decompile, or disassemble the Software, except to
34
+ the extent this restriction is unenforceable under applicable law;
35
+ (d) distribute, sublicense, rent, lease, lend, sell, or otherwise transfer
36
+ the Software to any third party;
37
+ (e) remove or obscure any proprietary notice contained in the Software; or
38
+ (f) use the Software to develop, or to assist any third party in
39
+ developing, a product or service competitive with the Service.
40
+
41
+ 4. OWNERSHIP
42
+
43
+ The Software is licensed, not sold. Licensor retains all right, title, and
44
+ interest in and to the Software, including all intellectual property rights
45
+ therein. No rights are granted to You except as expressly set out in this
46
+ License.
47
+
48
+ 5. THIRD-PARTY COMPONENTS
49
+
50
+ The Software may incorporate third-party open source components, each
51
+ governed by its own license terms. Those terms govern those components to
52
+ the extent they conflict with this License.
53
+
54
+ 6. TERMINATION
55
+
56
+ This License terminates automatically upon Your breach of any of its terms,
57
+ and terminates when Your written agreement with Licensor governing use of
58
+ the Service terminates or expires. Upon termination You must cease all use
59
+ of the Software and delete all copies in Your possession or control.
60
+
61
+ 7. NO WARRANTY
62
+
63
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
64
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
65
+ FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
66
+
67
+ 8. LIMITATION OF LIABILITY
68
+
69
+ TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL
70
+ LICENSOR BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN
71
+ ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN
72
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
73
+
74
+ 9. PRECEDENCE
75
+
76
+ Where You have a written agreement with Licensor governing use of the
77
+ Service, that agreement controls in the event of any conflict with this
78
+ License.
79
+
80
+ 10. GOVERNING LAW
81
+
82
+ This License is governed by the laws of India, without regard to its
83
+ conflict of law principles. The courts at Jaipur, Rajasthan, India shall
84
+ have exclusive jurisdiction, save where a written agreement between You and
85
+ Licensor governing use of the Service provides otherwise, in which case that
86
+ agreement controls.
87
+
88
+ Contact: hello@cygnos.ai
@@ -0,0 +1,194 @@
1
+ Metadata-Version: 2.4
2
+ Name: cygnos-capture-proxy
3
+ Version: 0.1.0
4
+ Summary: Local pass-through capture endpoint for AI API traffic — the first Cygnos capture source.
5
+ Author-email: Cygnos AI Private Limited <hello@cygnos.ai>
6
+ License-Expression: LicenseRef-Proprietary
7
+ Project-URL: Homepage, https://cygnos.ai
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Topic :: System :: Monitoring
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: starlette>=0.37
19
+ Requires-Dist: uvicorn>=0.29
20
+ Requires-Dist: httpx>=0.27
21
+ Requires-Dist: pydantic>=2
22
+ Requires-Dist: cygnos-shared[schema]>=0.1.0
23
+ Provides-Extra: brotli
24
+ Requires-Dist: brotli>=1.1; extra == "brotli"
25
+ Provides-Extra: zstd
26
+ Requires-Dist: zstandard>=0.22; extra == "zstd"
27
+ Provides-Extra: all
28
+ Requires-Dist: brotli>=1.1; extra == "all"
29
+ Requires-Dist: zstandard>=0.22; extra == "all"
30
+ Dynamic: license-file
31
+
32
+ # Connect your AI usage — Cygnos Capture Proxy
33
+
34
+ A lightweight local process that gives Cygnos visibility into your AI API usage.
35
+ It is a **pass-through reader, never a modifier**: it forwards every request to
36
+ the provider **unchanged** and emits one usage event per call to Cygnos. It
37
+ never filters, blocks, rewrites, retries, or injects anything. Claude Code is
38
+ the first supported client; more capture sources are coming.
39
+
40
+ > A bug in Cygnos is a bad dashboard, never a bad prompt — that's the whole
41
+ > design. The proxy only *observes*.
42
+
43
+ ---
44
+
45
+ ## Prerequisites (2 minutes)
46
+
47
+ 1. A **Cygnos account** and an **organisation that has been approved** (you'll
48
+ see the dashboard, not the "access under review" screen).
49
+ 2. An **API key**: in the dashboard, open **API Keys** → *Create key* → copy the
50
+ `cygnos_sk_…` value. You only see it once. (Events from an unapproved org are
51
+ rejected, so make sure your org is approved first.)
52
+
53
+ ## Install
54
+
55
+ ```bash
56
+ pip install cygnos-capture-proxy
57
+ ```
58
+
59
+ Python 3.10 or newer. That is the whole install — no Cygnos repository, no clone,
60
+ no build step. It brings its own dependencies and puts a `cygnos-capture` command
61
+ on your PATH.
62
+
63
+ gzip and deflate response streams are read out of the box. If your traffic comes
64
+ back brotli- or zstd-encoded, add the optional decoders (without them those
65
+ responses still pass through untouched — only the token counts on those events
66
+ are missed):
67
+
68
+ ```bash
69
+ pip install "cygnos-capture-proxy[all]"
70
+ ```
71
+
72
+ ## Configure (the two env vars)
73
+
74
+ ```bash
75
+ export CYGNOS_API_KEY="cygnos_sk_your_key_here"
76
+ export CYGNOS_INGEST_URL="https://<your-cygnos-host>/events"
77
+ ```
78
+
79
+ ## Run
80
+
81
+ ```bash
82
+ cygnos-capture # or: python -m cygnos_capture_proxy
83
+ ```
84
+
85
+ You'll see a startup line like:
86
+
87
+ ```
88
+ Cygnos Capture Proxy on http://127.0.0.1:8788 → upstream https://api.anthropic.com → ingest https://…/events (session 1f2e…)
89
+ ```
90
+
91
+ It binds to `127.0.0.1` only — nothing is exposed on your network.
92
+
93
+ ### The binding banner — read it before you walk away
94
+
95
+ Immediately after that line the proxy checks, against your Cygnos backend, whether
96
+ your key actually resolves to an organisation, and says so in plain words. This is
97
+ the one thing worth reading at startup, because a broken key is otherwise invisible:
98
+ your AI tools keep working perfectly while nothing reaches your dashboard.
99
+
100
+ A good binding looks like this — the organisation named is the tenant your events
101
+ will land under, so check it is the one you meant:
102
+
103
+ ```
104
+ Cygnos capture binding: OK — Your Cygnos API key was accepted.
105
+ Organisation: Acme Robotics (org_2f9c…)
106
+ Events will be sent to https://…/events
107
+ ```
108
+
109
+ A broken one is loud and tells you what to do about it:
110
+
111
+ ```
112
+ ───────────────────────────────────────────────────────────────────────────
113
+ CYGNOS CAPTURE IS NOT BOUND — captured events will NOT reach your dashboard.
114
+
115
+ What is wrong: Cygnos does not recognise this API key, or it has been revoked.
116
+ Every event will be rejected.
117
+ What to do: Check CYGNOS_API_KEY against the API Keys page. A key is shown once
118
+ at creation; if it was mangled or revoked, create a new one.
119
+
120
+ Your AI tools are UNAFFECTED. This proxy will keep forwarding every request
121
+ to the provider normally. Only Cygnos capture is broken.
122
+ ───────────────────────────────────────────────────────────────────────────
123
+ ```
124
+
125
+ A failed binding does **not** stop the proxy, on purpose: a Cygnos problem must never
126
+ become an outage in your day. It starts, it shouts, and it keeps forwarding your
127
+ traffic. If you would rather it refused to start than run unbound, set
128
+ `CYGNOS_REQUIRE_BINDING=1`.
129
+
130
+ ## Point Claude Code at it
131
+
132
+ In the **same terminal** where you'll run Claude Code:
133
+
134
+ ```bash
135
+ export ANTHROPIC_BASE_URL=http://localhost:8788
136
+ ```
137
+
138
+ Now use Claude Code exactly as normal. Your traffic flows through the proxy to
139
+ Anthropic untouched; Cygnos receives a usage event per request.
140
+
141
+ ## Confirm it's working
142
+
143
+ - The proxy's **startup banner** is printed and the process is still running.
144
+ - The terminal running Claude Code has `ANTHROPIC_BASE_URL` set
145
+ (`echo $ANTHROPIC_BASE_URL` → `http://localhost:8788`).
146
+ - Your **first event** appears in the Cygnos dashboard shortly after your first
147
+ Claude Code message.
148
+
149
+ **Am I being captured right now?** Capture happens only when *both* are true:
150
+ the proxy process is running, **and** the terminal you launched Claude Code from
151
+ had `ANTHROPIC_BASE_URL` exported. A different terminal (or after `unset`) talks
152
+ to Anthropic directly with no capture.
153
+
154
+ ## Stop / undo
155
+
156
+ ```bash
157
+ unset ANTHROPIC_BASE_URL # Claude Code instantly talks to Anthropic directly again
158
+ # then Ctrl-C the proxy process
159
+ ```
160
+
161
+ This is the one-line escape hatch — see the transport note below.
162
+
163
+ ## Troubleshooting
164
+
165
+ Run the proxy with `CYGNOS_CAPTURE_DEBUG=1` to log, per request, what it saw on
166
+ the wire (status, content-type, content-encoding, first bytes) and what it
167
+ extracted (model, tokens, status) — no prompt or response content is logged:
168
+
169
+ ```bash
170
+ CYGNOS_CAPTURE_DEBUG=1 python -m cygnos_capture_proxy
171
+ ```
172
+
173
+ If events show zero tokens, the debug log's `content_encoding` and `first_bytes`
174
+ lines are the place to look.
175
+
176
+ ---
177
+
178
+ ## Security & transport — read this once, honestly
179
+
180
+ - **The proxy sits in your request path.** While `ANTHROPIC_BASE_URL` points at
181
+ it, your Claude Code traffic goes *through* this local process. If the process
182
+ dies mid-request, that one in-flight request fails — the **one-line escape** is
183
+ `unset ANTHROPIC_BASE_URL` (Claude Code then talks to Anthropic directly). Event
184
+ emission is fully decoupled and fire-and-forget, so a Cygnos outage never
185
+ affects your traffic.
186
+ - **Your Anthropic API key never reaches Cygnos.** It passes straight through to
187
+ Anthropic. Cygnos receives only a one-way **hashed fingerprint** (16 hex chars)
188
+ so you can tell keys apart — never the key itself.
189
+ - **Your prompts and responses never leave your machine.** This proxy reads a
190
+ prompt only to forward it to Anthropic, and the response is never parsed for
191
+ text at all. Cygnos receives a one-way SHA-256 hash of the prompt and a
192
+ content-free risk verdict — PII reported by category name ("email"), never the
193
+ value; sensitive terms reported as a count, never the terms. This is enforced
194
+ in code: the proxy refuses to transmit any event carrying content.
@@ -0,0 +1,163 @@
1
+ # Connect your AI usage — Cygnos Capture Proxy
2
+
3
+ A lightweight local process that gives Cygnos visibility into your AI API usage.
4
+ It is a **pass-through reader, never a modifier**: it forwards every request to
5
+ the provider **unchanged** and emits one usage event per call to Cygnos. It
6
+ never filters, blocks, rewrites, retries, or injects anything. Claude Code is
7
+ the first supported client; more capture sources are coming.
8
+
9
+ > A bug in Cygnos is a bad dashboard, never a bad prompt — that's the whole
10
+ > design. The proxy only *observes*.
11
+
12
+ ---
13
+
14
+ ## Prerequisites (2 minutes)
15
+
16
+ 1. A **Cygnos account** and an **organisation that has been approved** (you'll
17
+ see the dashboard, not the "access under review" screen).
18
+ 2. An **API key**: in the dashboard, open **API Keys** → *Create key* → copy the
19
+ `cygnos_sk_…` value. You only see it once. (Events from an unapproved org are
20
+ rejected, so make sure your org is approved first.)
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pip install cygnos-capture-proxy
26
+ ```
27
+
28
+ Python 3.10 or newer. That is the whole install — no Cygnos repository, no clone,
29
+ no build step. It brings its own dependencies and puts a `cygnos-capture` command
30
+ on your PATH.
31
+
32
+ gzip and deflate response streams are read out of the box. If your traffic comes
33
+ back brotli- or zstd-encoded, add the optional decoders (without them those
34
+ responses still pass through untouched — only the token counts on those events
35
+ are missed):
36
+
37
+ ```bash
38
+ pip install "cygnos-capture-proxy[all]"
39
+ ```
40
+
41
+ ## Configure (the two env vars)
42
+
43
+ ```bash
44
+ export CYGNOS_API_KEY="cygnos_sk_your_key_here"
45
+ export CYGNOS_INGEST_URL="https://<your-cygnos-host>/events"
46
+ ```
47
+
48
+ ## Run
49
+
50
+ ```bash
51
+ cygnos-capture # or: python -m cygnos_capture_proxy
52
+ ```
53
+
54
+ You'll see a startup line like:
55
+
56
+ ```
57
+ Cygnos Capture Proxy on http://127.0.0.1:8788 → upstream https://api.anthropic.com → ingest https://…/events (session 1f2e…)
58
+ ```
59
+
60
+ It binds to `127.0.0.1` only — nothing is exposed on your network.
61
+
62
+ ### The binding banner — read it before you walk away
63
+
64
+ Immediately after that line the proxy checks, against your Cygnos backend, whether
65
+ your key actually resolves to an organisation, and says so in plain words. This is
66
+ the one thing worth reading at startup, because a broken key is otherwise invisible:
67
+ your AI tools keep working perfectly while nothing reaches your dashboard.
68
+
69
+ A good binding looks like this — the organisation named is the tenant your events
70
+ will land under, so check it is the one you meant:
71
+
72
+ ```
73
+ Cygnos capture binding: OK — Your Cygnos API key was accepted.
74
+ Organisation: Acme Robotics (org_2f9c…)
75
+ Events will be sent to https://…/events
76
+ ```
77
+
78
+ A broken one is loud and tells you what to do about it:
79
+
80
+ ```
81
+ ───────────────────────────────────────────────────────────────────────────
82
+ CYGNOS CAPTURE IS NOT BOUND — captured events will NOT reach your dashboard.
83
+
84
+ What is wrong: Cygnos does not recognise this API key, or it has been revoked.
85
+ Every event will be rejected.
86
+ What to do: Check CYGNOS_API_KEY against the API Keys page. A key is shown once
87
+ at creation; if it was mangled or revoked, create a new one.
88
+
89
+ Your AI tools are UNAFFECTED. This proxy will keep forwarding every request
90
+ to the provider normally. Only Cygnos capture is broken.
91
+ ───────────────────────────────────────────────────────────────────────────
92
+ ```
93
+
94
+ A failed binding does **not** stop the proxy, on purpose: a Cygnos problem must never
95
+ become an outage in your day. It starts, it shouts, and it keeps forwarding your
96
+ traffic. If you would rather it refused to start than run unbound, set
97
+ `CYGNOS_REQUIRE_BINDING=1`.
98
+
99
+ ## Point Claude Code at it
100
+
101
+ In the **same terminal** where you'll run Claude Code:
102
+
103
+ ```bash
104
+ export ANTHROPIC_BASE_URL=http://localhost:8788
105
+ ```
106
+
107
+ Now use Claude Code exactly as normal. Your traffic flows through the proxy to
108
+ Anthropic untouched; Cygnos receives a usage event per request.
109
+
110
+ ## Confirm it's working
111
+
112
+ - The proxy's **startup banner** is printed and the process is still running.
113
+ - The terminal running Claude Code has `ANTHROPIC_BASE_URL` set
114
+ (`echo $ANTHROPIC_BASE_URL` → `http://localhost:8788`).
115
+ - Your **first event** appears in the Cygnos dashboard shortly after your first
116
+ Claude Code message.
117
+
118
+ **Am I being captured right now?** Capture happens only when *both* are true:
119
+ the proxy process is running, **and** the terminal you launched Claude Code from
120
+ had `ANTHROPIC_BASE_URL` exported. A different terminal (or after `unset`) talks
121
+ to Anthropic directly with no capture.
122
+
123
+ ## Stop / undo
124
+
125
+ ```bash
126
+ unset ANTHROPIC_BASE_URL # Claude Code instantly talks to Anthropic directly again
127
+ # then Ctrl-C the proxy process
128
+ ```
129
+
130
+ This is the one-line escape hatch — see the transport note below.
131
+
132
+ ## Troubleshooting
133
+
134
+ Run the proxy with `CYGNOS_CAPTURE_DEBUG=1` to log, per request, what it saw on
135
+ the wire (status, content-type, content-encoding, first bytes) and what it
136
+ extracted (model, tokens, status) — no prompt or response content is logged:
137
+
138
+ ```bash
139
+ CYGNOS_CAPTURE_DEBUG=1 python -m cygnos_capture_proxy
140
+ ```
141
+
142
+ If events show zero tokens, the debug log's `content_encoding` and `first_bytes`
143
+ lines are the place to look.
144
+
145
+ ---
146
+
147
+ ## Security & transport — read this once, honestly
148
+
149
+ - **The proxy sits in your request path.** While `ANTHROPIC_BASE_URL` points at
150
+ it, your Claude Code traffic goes *through* this local process. If the process
151
+ dies mid-request, that one in-flight request fails — the **one-line escape** is
152
+ `unset ANTHROPIC_BASE_URL` (Claude Code then talks to Anthropic directly). Event
153
+ emission is fully decoupled and fire-and-forget, so a Cygnos outage never
154
+ affects your traffic.
155
+ - **Your Anthropic API key never reaches Cygnos.** It passes straight through to
156
+ Anthropic. Cygnos receives only a one-way **hashed fingerprint** (16 hex chars)
157
+ so you can tell keys apart — never the key itself.
158
+ - **Your prompts and responses never leave your machine.** This proxy reads a
159
+ prompt only to forward it to Anthropic, and the response is never parsed for
160
+ text at all. Cygnos receives a one-way SHA-256 hash of the prompt and a
161
+ content-free risk verdict — PII reported by category name ("email"), never the
162
+ value; sensitive terms reported as a count, never the terms. This is enforced
163
+ in code: the proxy refuses to transmit any event carrying content.
@@ -0,0 +1,18 @@
1
+ """
2
+ Cygnos Capture Proxy — a local, pass-through capture endpoint for AI API traffic.
3
+
4
+ This is the first "capture source" in Cygnos' connect-your-usage portfolio. A
5
+ customer runs it locally; an AI tool points at it via a base-URL override
6
+ (Claude Code's ANTHROPIC_BASE_URL is the first documented client). The proxy
7
+ forwards every request to the provider UNCHANGED and emits one standard Cygnos
8
+ event per completion to the customer's ingestion endpoint.
9
+
10
+ North-star constraints (see DF1_ONBOARDING.md BI-2):
11
+ - Pass-through reader, NEVER a modifier — no filter/block/rewrite/inject/retry.
12
+ - Fail-open, fail-silent — event emission is fire-and-forget and never affects
13
+ the proxied request. (The proxy is in the request path, so if the *process*
14
+ dies a single in-flight request drops; the escape is to unset the env var.)
15
+ - Standard events only — the existing Universal AI Event Schema, no new type.
16
+ - Keyed ingestion — CYGNOS_API_KEY + CYGNOS_INGEST_URL; the provider key
17
+ passes through to the provider and never reaches Cygnos (fingerprint only).
18
+ """
@@ -0,0 +1,74 @@
1
+ """
2
+ Entry point: `python -m cygnos_capture_proxy`.
3
+
4
+ Starts the local pass-through capture endpoint on 127.0.0.1:<port>. Point your
5
+ AI tool at it, e.g. for Claude Code: export ANTHROPIC_BASE_URL=http://localhost:8788
6
+ To stop capturing instantly: unset ANTHROPIC_BASE_URL (Claude Code talks to the
7
+ provider directly again) and Ctrl-C this process.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ import sys
13
+
14
+ import uvicorn
15
+
16
+ from .app import build_app
17
+ from .config import ConfigFileError, config_path, load_config
18
+ from .portcheck import check_or_explain
19
+
20
+
21
+ def main() -> None:
22
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-8s %(message)s")
23
+ try:
24
+ cfg = load_config()
25
+ except ConfigFileError as exc:
26
+ # CFG-1 §5 — a config file we cannot trust stops the proxy, and stops it
27
+ # READABLY. Printed rather than raised: a traceback through json.loads tells
28
+ # an operator nothing they can act on, while this names the file and the line.
29
+ # Exit 1 so a supervisor (launchd, systemd, a shell script) sees a failure
30
+ # rather than a clean stop.
31
+ print(f"\nCygnos capture proxy did not start.\n\n{exc}\n", file=sys.stderr)
32
+ raise SystemExit(1)
33
+ # CAPTURE-1 — the old "CYGNOS_API_KEY is not set" warning lived here and was
34
+ # the ONLY signal the proxy ever gave about its binding. It has been replaced
35
+ # by the startup binding check in the app lifespan (see binding.py), which
36
+ # covers the unset key AND the three failures this could never see: a key
37
+ # this backend does not recognise, a key whose organisation is not approved,
38
+ # and a backend that does not answer at all.
39
+ # CFG-1 — say which file was consulted, even when there was none. An operator
40
+ # debugging "why is it using that URL" needs to know whether a file is in play at
41
+ # all; silence about the file is what makes stale-config bugs hard to see.
42
+ _path = config_path()
43
+ logging.info(
44
+ "Cygnos config file: %s",
45
+ _path if _path.exists() else f"{_path} (not present — environment and defaults only)",
46
+ )
47
+ logging.info(
48
+ "Cygnos Capture Proxy on http://%s:%d → upstream %s → ingest %s (session %s)",
49
+ cfg.host, cfg.port, cfg.upstream_base_url, cfg.ingest_url, cfg.session_id,
50
+ )
51
+ # INIT-1 §6 — refuse to start on a squatted port, LOUDLY.
52
+ #
53
+ # uvicorn's own failure here is a raw OSError traceback ending "address already
54
+ # in use", and the damage is not the ugly traceback. It is the belief it
55
+ # leaves behind: the operator restarts the proxy after changing their key or
56
+ # their actor, sees a wall of red they read as a crash, and the OLD proxy is
57
+ # still running, still capturing, still on the old configuration. They now
58
+ # believe the restart failed when in fact it never happened — and every event
59
+ # from that point is stamped with settings nobody thinks are in effect.
60
+ #
61
+ # So we look BEFORE binding, name what is holding the port, and say which of
62
+ # the two cases it is. check_or_explain returns None when the port is free
63
+ # AND when its own inspection fails, because a diagnostic that cannot run
64
+ # must never become the reason the proxy will not start (CAPTURE-1).
65
+ collision = check_or_explain(cfg.host, cfg.port)
66
+ if collision:
67
+ print("\n" + "\n".join(collision) + "\n", file=sys.stderr)
68
+ raise SystemExit(1)
69
+
70
+ uvicorn.run(build_app(cfg), host=cfg.host, port=cfg.port, log_level="info")
71
+
72
+
73
+ if __name__ == "__main__":
74
+ main()