agent-interlude 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,7 @@
1
+ CLAUDE.md
2
+ AGENTS.md
3
+ .agent-interlude/
4
+ .interlude/
5
+ .venv/
6
+ __pycache__/
7
+ dist/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023-2026 Zonda Yang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,397 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-interlude
3
+ Version: 0.1.0
4
+ Summary: Intercept and log AI coding agent <-> API traffic for prompt-architecture analysis.
5
+ Project-URL: Homepage, https://github.com/zondatw/agent-interlude
6
+ Project-URL: Repository, https://github.com/zondatw/agent-interlude
7
+ Project-URL: Issues, https://github.com/zondatw/agent-interlude/issues
8
+ Author: Zonda Yang
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: ai-agents,claude-code,codex,llm-observability,prompt-analysis,reverse-proxy,sse
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Environment :: Web Environment
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Internet :: Proxy Servers
23
+ Classifier: Topic :: Software Development :: Debuggers
24
+ Requires-Python: >=3.11
25
+ Description-Content-Type: text/markdown
26
+
27
+ # agent-interlude
28
+
29
+ English · **[繁體中文](README.zh-TW.md)**
30
+
31
+ agent-interlude intercepts the traffic between an AI coding agent (**Claude Code**,
32
+ **Codex**) and its API, persisting the prompt structure (`system` / `tools` /
33
+ `messages`) of every request/response pair as JSONL. Use it to analyze the
34
+ fixed skeleton vs. dynamic slots of a prompt, and to compare across agents.
35
+
36
+ ## How it works
37
+
38
+ Both agents let you override the API base URL via an environment variable, so
39
+ no transparent MITM or certificate forgery is needed. agent-interlude is an
40
+ **explicit reverse proxy**:
41
+
42
+ ```
43
+ Claude Code ──(A) plain HTTP──▶ agent-interlude proxy ──(B) normal HTTPS──▶ api.anthropic.com
44
+ localhost:8788 (proxy re-encrypts as the client)
45
+ ```
46
+
47
+ Segment (A) has no TLS, so the proxy reads the plaintext body straight off the
48
+ socket — that's the interception point, and it never touches credentials.
49
+ Responses are copied as they stream through a relay, then the SSE events are
50
+ reassembled and archived once the stream ends. The agent notices nothing.
51
+
52
+ ## Install
53
+
54
+ Install once with [`pipx`](https://pipx.pypa.io/) (recommended) or
55
+ [`uv tool`](https://docs.astral.sh/uv/concepts/tools/) — both put the
56
+ `agent-interlude` command on your PATH in an isolated environment, no
57
+ project-level setup needed:
58
+
59
+ ```bash
60
+ pipx install agent-interlude
61
+ # or
62
+ uv tool install agent-interlude
63
+ ```
64
+
65
+ Requires Python 3.11+ and the agent CLIs you want to capture
66
+ (`claude` and/or `codex`). Zero runtime dependencies — agent-interlude is
67
+ stdlib-only.
68
+
69
+ For contributors hacking on agent-interlude itself, see
70
+ [Development setup](#development-setup) below.
71
+
72
+ ## Quick start
73
+
74
+ ```bash
75
+ # 1. One command: starts the 3 proxy listeners AND the web UI on :8000
76
+ agent-interlude
77
+
78
+ # 2. In another terminal, point Claude Code at it
79
+ ANTHROPIC_BASE_URL=http://localhost:8788 claude
80
+
81
+ # 3. Open the live browser UI as captures stream in
82
+ open http://127.0.0.1:8000/timeline
83
+ ```
84
+
85
+ On startup the bundled launcher prints:
86
+
87
+ ```
88
+ [agent-interlude] claude: http://127.0.0.1:8788 -> https://api.anthropic.com
89
+ [agent-interlude] codex: http://127.0.0.1:8789 -> https://api.openai.com (Codex + API key)
90
+ [agent-interlude] codex: http://127.0.0.1:8790 -> https://chatgpt.com (Codex + ChatGPT login)
91
+ [agent-interlude] logging to .agent-interlude/log-<timestamp>.jsonl
92
+ [agent-interlude] web UI: http://127.0.0.1:8000/timeline (auto-started; disable with --no-ui)
93
+ [agent-interlude-report] http://127.0.0.1:8000
94
+ [agent-interlude-report] watching .agent-interlude/log-*.jsonl
95
+ [agent-interlude-report] auto-reload on (disable with --no-reload)
96
+ ```
97
+
98
+ `.agent-interlude/log-<timestamp>.jsonl` lands under your current working
99
+ directory (not next to the installed module), so run `agent-interlude` from
100
+ wherever you want the logs collected.
101
+
102
+ The web UI runs in a child process. `Ctrl-C` on `agent-interlude` tears down
103
+ both proxy and UI cleanly.
104
+
105
+ Each launch opens a fresh log file; every request prints one line such as
106
+ `[claude] POST /v1/messages`.
107
+
108
+ Variants:
109
+
110
+ ```bash
111
+ agent-interlude --no-ui # proxy-only (e.g. CI / headless capture)
112
+ agent-interlude --ui-port 9000 # bind the UI on a different port
113
+ agent-interlude-report serve # UI only, against existing logs
114
+ agent-interlude-analyze # text report, no server
115
+ python -m agent_interlude # module-form, equivalent to `agent-interlude`
116
+ ```
117
+
118
+ ## Development setup
119
+
120
+ To hack on agent-interlude itself, clone and use the source layout directly:
121
+
122
+ ```bash
123
+ git clone https://github.com/zondatw/agent-interlude.git
124
+ cd agent-interlude
125
+ uv sync # installs the package in editable mode
126
+ uv run agent-interlude # runs from src/agent_interlude/
127
+ ```
128
+
129
+ For contributors: install [`pre-commit`](https://pre-commit.com/) and
130
+ [`gitleaks`](https://github.com/gitleaks/gitleaks) (`brew install pre-commit gitleaks`),
131
+ then run `pre-commit install` once. Subsequent `git commit` will auto-run
132
+ ruff lint+format, hygiene checks (trailing whitespace, EOF, private keys,
133
+ yaml/toml syntax), codespell, and gitleaks. Run `pre-commit run --all-files`
134
+ to check the whole tree at once.
135
+
136
+ Release flow (PyPI Trusted Publishing via the `beta` and `release`
137
+ branches) is documented in [`docs/release.md`](docs/release.md).
138
+
139
+ ## Pointing an agent at the proxy
140
+
141
+ ### Claude Code
142
+
143
+ An environment variable is enough (Claude Code appends `/v1/messages` to the
144
+ base URL itself):
145
+
146
+ ```bash
147
+ ANTHROPIC_BASE_URL=http://localhost:8788 claude
148
+ # Non-interactive:
149
+ ANTHROPIC_BASE_URL=http://localhost:8788 claude -p "say hi"
150
+ ```
151
+
152
+ ### Codex
153
+
154
+ Codex's built-in `openai` provider **does not honor** a base-URL override
155
+ (`OPENAI_BASE_URL` is ignored), so you must define a custom provider. Pick the
156
+ route that matches your login method.
157
+
158
+ #### A. ChatGPT login (recommended, no API key)
159
+
160
+ Point the custom provider at the proxy's **chatgpt.com** listener (port 8790,
161
+ path `/backend-api/codex`). Codex sends your ChatGPT token, the proxy forwards
162
+ to the real `https://chatgpt.com/backend-api/codex/responses`, and the response
163
+ is recorded too:
164
+
165
+ ```bash
166
+ codex exec -s read-only \
167
+ -c model_provider=agent-interlude \
168
+ -c 'model_providers.agent-interlude.base_url="http://localhost:8790/backend-api/codex"' \
169
+ -c 'model_providers.agent-interlude.wire_api="responses"' \
170
+ "say hi"
171
+ ```
172
+
173
+ For a durable setup, write it into `~/.codex/config.toml`:
174
+
175
+ ```toml
176
+ [model_providers.agent-interlude]
177
+ name = "agent-interlude"
178
+ base_url = "http://localhost:8790/backend-api/codex"
179
+ wire_api = "responses"
180
+ ```
181
+
182
+ Then switch to it per-invocation with `-c model_provider=agent-interlude` (do **not**
183
+ set a top-level `model_provider`, or Codex breaks whenever the proxy is down):
184
+
185
+ ```bash
186
+ codex -c model_provider=agent-interlude exec -s read-only "say hi"
187
+ ```
188
+
189
+ #### B. OpenAI API key
190
+
191
+ If you have an `OPENAI_API_KEY` with the `api.responses.write` scope, point at
192
+ the proxy's **api.openai.com** listener instead (port 8789, path `/v1`):
193
+
194
+ ```bash
195
+ codex exec -s read-only \
196
+ -c model_provider=agent-interlude \
197
+ -c 'model_providers.agent-interlude.base_url="http://localhost:8789/v1"' \
198
+ -c 'model_providers.agent-interlude.wire_api="responses"' \
199
+ "say hi"
200
+ ```
201
+
202
+ > **Note** — Using ChatGPT login but pointing at `api.openai.com` (route B
203
+ > without a key) returns **401** (the ChatGPT token lacks the
204
+ > `api.responses.write` scope). The request is still recorded in full; you just
205
+ > get no response back — switch to route A instead.
206
+
207
+ ## What gets recorded
208
+
209
+ Logs live in `.agent-interlude/log-<timestamp>.jsonl`. Each exchange is **two lines**
210
+ paired by `id`:
211
+
212
+ ```jsonc
213
+ // kind="request"
214
+ {"id":"ab12…","kind":"request","agent":"claude","wire":"claude-messages",
215
+ "headers_kept":{…}, // authorization / x-api-key already filtered out
216
+ "request":{…full parsed body…},
217
+ "extract":{"system":…,"tools":…,"messages":…}}
218
+
219
+ // kind="response" (same id)
220
+ {"id":"ab12…","kind":"response","agent":"claude","status":200,
221
+ "stream":true,"event_count":7,"event_types":{…},
222
+ "reconstructed":{"model":"…","text":"…","usage":{…},"tool_uses":[…]}}
223
+ ```
224
+
225
+ A non-streaming response (e.g. Codex's 401) is recorded as
226
+ `"stream":false,"body":{…}` instead.
227
+
228
+ Supported wire formats: `claude-messages` (`/v1/messages`), `codex-responses`
229
+ (`/responses`), `codex-chat` (`/chat/completions`).
230
+
231
+ ## Analysis
232
+
233
+ ```bash
234
+ agent-interlude-analyze # read every log in .agent-interlude
235
+ agent-interlude-analyze --agent claude # one agent only
236
+ agent-interlude-analyze --max-slots 30 # print more dynamic slots
237
+ agent-interlude-analyze path/to/log.jsonl # a specific file / glob
238
+ ```
239
+
240
+ The report covers:
241
+
242
+ - Each agent's system size and **fixed skeleton vs. dynamic slots** (e.g. the
243
+ `git status` and date that Claude injects are flagged as dynamic slots).
244
+ - The tools list, count, and schema key (Claude=`input_schema` /
245
+ Codex=`parameters`).
246
+ - A cross-agent structure comparison table.
247
+
248
+ > To surface Codex's dynamic slots, run a few sessions with **different prompts /
249
+ > at different times** (multiple retries of the same prompt share one system
250
+ > prompt and count as just 1 distinct sample).
251
+
252
+ ## Web UI
253
+
254
+ For a browsable view of the same data — with per-request drill-in,
255
+ skeleton-vs-slot highlighting in context, and a tools schema browser —
256
+ launch the local web UI:
257
+
258
+ ```bash
259
+ agent-interlude-report serve # http://127.0.0.1:8000 (default)
260
+ agent-interlude-report serve --port 9000
261
+ agent-interlude-report serve --logs "other/path/log-*.jsonl"
262
+ ```
263
+
264
+ Routes:
265
+
266
+ | Path | What it shows |
267
+ |---|---|
268
+ | `/` | Cross-agent overview + per-agent stats |
269
+ | `/timeline[?agent=…&since=…&from=…&to=…&session_gap=…]` | Sequence-diagram view of every exchange: agent ↔ API lanes, two arrows per exchange (request + response), auto-grouped into sessions (gap-threshold configurable), per-hour density histogram on top, RTT bars on every response arrow. Click an arrow to expand only that half (request → system/tools/messages; response → reassembled text/usage/event_types). |
270
+ | `/requests[?agent=…]` | Sortable list of exchanges with model / token columns |
271
+ | `/requests/<id>` | Collapsible system / tools / messages + paired reassembled response |
272
+ | `/skeleton/<agent>` | Canonical system sample with fixed lines greyed and dynamic slots highlighted in yellow |
273
+ | `/tools/<agent>` | Collapsible JSON schema per tool |
274
+
275
+ Every HTML page has a matching `/api/<same path>` endpoint that returns the
276
+ same data as JSON — built in from day one so future features (token usage
277
+ charts, search/filter, live update) consume a stable backend instead of
278
+ re-scraping HTML. The page nav surfaces the JSON URL on every view.
279
+
280
+ Bound to `127.0.0.1` only (the logs hold full prompts; never expose them on
281
+ LAN). The JSONL loader is mtime-cached, so re-reads stay cheap while the
282
+ proxy keeps appending — just refresh the page to see new captures.
283
+
284
+ ## One-command end-to-end verification
285
+
286
+ ```bash
287
+ ./dogfood.sh
288
+ ```
289
+
290
+ Starts the proxy → fires one Claude and one Codex call → verifies both request
291
+ and response were recorded with zero credential leakage → tears the proxy down,
292
+ and finally prints `RESULT: PASS`.
293
+
294
+ ## Manual verification (step by step)
295
+
296
+ To confirm each link in the chain by hand (rather than just running
297
+ `dogfood.sh`):
298
+
299
+ **Terminal 1** — start the proxy:
300
+
301
+ ```bash
302
+ agent-interlude
303
+ ```
304
+
305
+ **Terminal 2** — send one message through Claude Code; it should reply `PONG`
306
+ (proving the relay + streaming are intact):
307
+
308
+ ```bash
309
+ ANTHROPIC_BASE_URL=http://localhost:8788 claude -p "Reply with exactly the word PONG and nothing else."
310
+ ```
311
+
312
+ Back in Terminal 1, the proxy console should show a line:
313
+ `[claude] POST /v1/messages`.
314
+
315
+ **Check the log landed** (a structural summary that does not dump prompt
316
+ contents):
317
+
318
+ ```bash
319
+ LOG=$(ls -t .agent-interlude/log-*.jsonl | head -1)
320
+ uv run python - "$LOG" <<'PY'
321
+ import json, re, sys
322
+ recs = [json.loads(l) for l in open(sys.argv[1], encoding="utf-8")]
323
+ for r in recs:
324
+ if r.get("kind", "request") == "request":
325
+ ex = r.get("extract") or {}
326
+ present = [k for k in ("system", "tools", "messages") if ex.get(k) is not None]
327
+ print(f"REQ {r['agent']:<7} {r['wire']:<16} extract={present}")
328
+ else:
329
+ txt = (r.get("reconstructed") or {}).get("text")
330
+ info = f"text={txt!r}" if r.get("stream") else f"body={type(r.get('body')).__name__}"
331
+ print(f"RESP {r['agent']:<7} status={r['status']:<3} {info[:70]}")
332
+ blob = "\n".join(json.dumps(r) for r in recs)
333
+ leaks = re.findall(r"Bearer\s+\S{20,}|sk-ant-\S{20,}|eyJ[\w-]{10,}\.eyJ[\w-]{10,}", blob)
334
+ print("\ncredential leaks:", len(leaks))
335
+ PY
336
+ ```
337
+
338
+ You should see at least one pair:
339
+
340
+ ```
341
+ REQ claude claude-messages extract=['system', 'tools', 'messages']
342
+ RESP claude status=200 text='PONG'
343
+
344
+ credential leaks: 0
345
+ ```
346
+
347
+ (The first line may be `REQ claude unknown` → `RESP claude status=404`; that's
348
+ Claude Code's connection pre-check `HEAD /` and can be ignored.)
349
+
350
+ **View the structure analysis**:
351
+
352
+ ```bash
353
+ agent-interlude-analyze
354
+ ```
355
+
356
+ When done, press `Ctrl-C` in Terminal 1 to shut the proxy down.
357
+
358
+ ## Security notes
359
+
360
+ - `.agent-interlude/` contains the **full prompt** (your code, possibly secrets) → it
361
+ is gitignored; **do not commit or share it.**
362
+ - Auth headers (`authorization` / `x-api-key` / `cookie`) are forwarded only and
363
+ **never written to the log**; `headers_kept` retains an allowlist of fields
364
+ only.
365
+ - The proxy strips the request's `accept-encoding`, so the recorded bytes are
366
+ always plaintext (no gzip/br to deal with).
367
+ - To check for rogue connections: `lsof -nP -iTCP -sTCP:ESTABLISHED | grep
368
+ Python`, and confirm the proxy only connects to `api.anthropic.com` /
369
+ `api.openai.com` / `chatgpt.com`.
370
+
371
+ ## Adding a new agent
372
+
373
+ Edit the `LISTENERS` list at the top of `src/agent_interlude/proxy.py` and add a
374
+ row `(port, upstream_host, label)`. Wire detection lives in `detect_wire()`,
375
+ and field normalization in `extract()` (requests) and `reconstruct()`
376
+ (responses).
377
+
378
+ ## Troubleshooting
379
+
380
+ | Symptom | Cause / fix |
381
+ |---|---|
382
+ | `port 8788 already in use` | A previous proxy is still running. `lsof -nP -iTCP:8788 -sTCP:LISTEN` to find the PID → `kill <PID>` |
383
+ | Codex isn't recorded, startup prints `provider: openai` | You used the `OPENAI_BASE_URL` shortcut, which the built-in provider ignores. Use a custom provider instead |
384
+ | Codex returns 401 (missing `api.responses.write` scope) | You're on ChatGPT login but pointing at `api.openai.com` (8789). Switch to route A (8790 + `/backend-api/codex`); see "Pointing an agent at the proxy › Codex" |
385
+ | Agent refuses `http://` | Fall back to TLS: the proxy terminates TLS with a self-signed CA, and Claude Code trusts it via `NODE_EXTRA_CA_CERTS` (not needed currently) |
386
+
387
+ ## Files
388
+
389
+ | Path | Purpose |
390
+ |---|---|
391
+ | `src/agent_interlude/proxy.py` | Three-listener reverse proxy, streaming relay + SSE tee/reassembly. Entry point: `agent-interlude` |
392
+ | `src/agent_interlude/analyze.py` | Cross-request diff, fixed skeleton vs. dynamic slots, cross-agent comparison (text report). Entry point: `agent-interlude-analyze` |
393
+ | `src/agent_interlude/report.py` | Local web UI (HTML + JSON) over the same analysis. Entry point: `agent-interlude-report` |
394
+ | `dogfood.sh` | One-command end-to-end verification (contributor-facing, not shipped in the wheel) |
395
+ | `docs/release.md` | PyPI Trusted Publishing setup + per-release flow |
396
+ | `.github/workflows/` | `beta.yml` (push to `beta` → test.pypi.org), `release.yml` (push to `release` → pypi.org) |
397
+ | `.agent-interlude/` | JSONL output, written under the user's cwd (gitignored, sensitive) |