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.
- cygnos_capture_proxy-0.1.0/LICENSE +88 -0
- cygnos_capture_proxy-0.1.0/PKG-INFO +194 -0
- cygnos_capture_proxy-0.1.0/README.md +163 -0
- cygnos_capture_proxy-0.1.0/__init__.py +18 -0
- cygnos_capture_proxy-0.1.0/__main__.py +74 -0
- cygnos_capture_proxy-0.1.0/adapters.py +232 -0
- cygnos_capture_proxy-0.1.0/app.py +373 -0
- cygnos_capture_proxy-0.1.0/binding.py +512 -0
- cygnos_capture_proxy-0.1.0/classifier.py +121 -0
- cygnos_capture_proxy-0.1.0/cli.py +851 -0
- cygnos_capture_proxy-0.1.0/config.py +785 -0
- cygnos_capture_proxy-0.1.0/cygnos_capture_proxy.egg-info/PKG-INFO +194 -0
- cygnos_capture_proxy-0.1.0/cygnos_capture_proxy.egg-info/SOURCES.txt +33 -0
- cygnos_capture_proxy-0.1.0/cygnos_capture_proxy.egg-info/dependency_links.txt +1 -0
- cygnos_capture_proxy-0.1.0/cygnos_capture_proxy.egg-info/entry_points.txt +3 -0
- cygnos_capture_proxy-0.1.0/cygnos_capture_proxy.egg-info/requires.txt +15 -0
- cygnos_capture_proxy-0.1.0/cygnos_capture_proxy.egg-info/top_level.txt +1 -0
- cygnos_capture_proxy-0.1.0/events.py +520 -0
- cygnos_capture_proxy-0.1.0/portcheck.py +272 -0
- cygnos_capture_proxy-0.1.0/pyproject.toml +69 -0
- cygnos_capture_proxy-0.1.0/service.py +464 -0
- cygnos_capture_proxy-0.1.0/setup.cfg +4 -0
- cygnos_capture_proxy-0.1.0/tools.py +418 -0
|
@@ -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()
|