Skip to content

Commit 7944ddb

Browse files
feat: enforce debug/host guard and document API versioning (#60)
* feat: enforce debug/host guard and document API versioning * feat: enforce debug/host guard and document API versioning * fix: address PR review on debug guard and API docs - Simplify 127.x.x.x loopback check; reject empty host in format_listen_url - Add malformed 127.* tests; search endpoint stability column - CHANGELOG [0.1.0] footer link; SPA two-release deprecation note - README inline warning on 0.0.0.0 example * docs: address PR review on debug guard scope and deprecation wording * fix: address PR review on loopback host, versioning, and docs * docs: add Keep a Changelog [Unreleased] compare link
1 parent 4b2a4c6 commit 7944ddb

7 files changed

Lines changed: 273 additions & 59 deletions

File tree

CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Changelog
2+
3+
All notable changes to this project are documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6+
7+
## [Unreleased]
8+
9+
### Added
10+
11+
- `__version__` in `app.py` for release tracking (`0.1.0.dev0` until the first `v0.1.0` git tag)
12+
- Startup guard refusing `--debug` with a non-loopback `--host` (including bracketed IPv6 loopback such as `[::1]`)
13+
- [Deprecation policy](docs/deprecation-policy.md) for API and JSON field changes
14+
- API field **stability** tables in `docs/api-reference.md` (stable / experimental / deprecated)
15+
16+
### Changed
17+
18+
- README notes that the server enforces the debug + host safety rule at startup
19+
20+
### Deprecated
21+
22+
- `export_count` on `GET /api/export/state` (documented only; still returned). Use `last_export_session_count`. Removal planned in a follow-up release per [deprecation policy](docs/deprecation-policy.md).
23+
24+
[Unreleased]: https://github.com/cppalliance/claude-code-chat-browser/compare/f70505982d435f8b1f754cb18c0c9f65609f11b4...HEAD

CONTRIBUTING.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,16 @@ Useful flags:
4040

4141
- `--base-dir PATH` — point at a different `projects/` tree (for tests or fixtures)
4242
- `--exclude-rules PATH` — session exclusion rules file
43-
- `--host 0.0.0.0` — listen on all interfaces (use only on trusted networks)
43+
- `--host 0.0.0.0` — listen on all interfaces (use only on trusted networks; never with `--debug`)
44+
- `--debug` — Flask/Werkzeug debug mode (loopback hosts only; enforced when starting via `python app.py`, not `flask run` or WSGI). Extending the guard to `FLASK_DEBUG` / `flask run` is a planned follow-up.
45+
46+
## API and release policy
47+
48+
- [CHANGELOG.md](CHANGELOG.md) — user-visible changes per release
49+
- [docs/deprecation-policy.md](docs/deprecation-policy.md) — how deprecated API fields are removed
50+
- [docs/api-reference.md](docs/api-reference.md) — field **stability** (`stable` / `experimental` / `deprecated`)
51+
52+
When changing JSON response shapes, update the API reference stability column and CHANGELOG before removing fields.
4453

4554
## Running tests
4655

@@ -116,6 +125,8 @@ npm run test:coverage # optional
116125
| SPA shell + routing | [`static/index.html`](static/index.html), [`static/js/app.js`](static/js/app.js) |
117126
| Shared frontend utilities | [`static/js/shared/`](static/js/shared/) |
118127
| API documentation | [`docs/api-reference.md`](docs/api-reference.md) |
128+
| Deprecation policy | [`docs/deprecation-policy.md`](docs/deprecation-policy.md) |
129+
| Changelog | [`CHANGELOG.md`](CHANGELOG.md) |
119130

120131
## Architecture
121132

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,16 @@ python app.py
5858

5959
Options:
6060
```bash
61-
python app.py --port 8080 --host 0.0.0.0
61+
python app.py --port 8080 --host 0.0.0.0 # never add --debug on 0.0.0.0
6262
python app.py --base-dir /path/to/claude/projects
6363
```
6464

6565
> **Security warning:** Do not use `--host 0.0.0.0` together with `--debug` on untrusted networks.
6666
> That combination exposes [Werkzeug's interactive debugger](https://werkzeug.palletsprojects.com/en/stable/debug/),
6767
> which allows arbitrary code execution from any browser that can reach the server.
6868
> For typical local browsing, keep the default `--host 127.0.0.1` and omit `--debug`.
69+
> The server **refuses to start** if `--debug` is combined with a non-loopback `--host` (e.g. `0.0.0.0`).
70+
> That check runs only when you start the app with **`python app.py`** (not via `flask run` or other WSGI entrypoints).
6971
7072
### CLI Export
7173

app.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Flask app that serves the web GUI for browsing sessions."""
22

3+
__version__ = "0.1.0.dev0"
4+
35
import argparse
46
import os
57
import sys
@@ -13,6 +15,59 @@
1315
from utils.exclusion_rules import resolve_exclusion_rules_path, load_rules
1416

1517

18+
def _normalize_bind_host(host: str) -> str:
19+
"""Lowercase host for checks; strip optional IPv6 brackets (e.g. ``[::1]`` → ``::1``)."""
20+
h = (host or "").strip().lower()
21+
if len(h) >= 2 and h.startswith("[") and h.endswith("]"):
22+
return h[1:-1]
23+
return h
24+
25+
26+
def is_loopback_host(host: str) -> bool:
27+
"""True if ``host`` binds only to the local machine (safe with ``--debug``).
28+
29+
Accepts ``127.0.0.1``, ``localhost``, ``::1``, ``[::1]``, and other ``127.x.x.x`` addresses.
30+
Rejects all-interfaces forms such as ``0.0.0.0`` and bare ``::`` (not loopback).
31+
"""
32+
h = _normalize_bind_host(host)
33+
if h in ("127.0.0.1", "localhost", "::1"):
34+
return True
35+
if h.startswith("127.") and h.count(".") == 3:
36+
parts = h.split(".")
37+
try:
38+
return all(0 <= int(p) <= 255 for p in parts)
39+
except ValueError:
40+
return False
41+
return False
42+
43+
44+
def format_listen_url(host: str, port: int) -> str:
45+
"""Return a valid ``http://`` URL for the startup banner (IPv6 hosts bracketed)."""
46+
h = (host or "").strip()
47+
if not h:
48+
raise ValueError("host must not be empty")
49+
if h.startswith("[") and h.endswith("]"):
50+
display_host = h
51+
elif ":" in h:
52+
display_host = f"[{h}]"
53+
else:
54+
display_host = h
55+
return f"http://{display_host}:{port}"
56+
57+
58+
def validate_startup_cli(args: argparse.Namespace) -> None:
59+
"""Refuse ``--debug`` when ``--host`` is reachable off loopback."""
60+
if args.debug and not is_loopback_host(args.host):
61+
print(
62+
"error: --debug is only allowed with a loopback --host "
63+
"(127.0.0.1, localhost, ::1, [::1], or 127.x.x.x). "
64+
"Combining --debug with a network-visible --host exposes the "
65+
"Werkzeug debugger and session data to other machines.",
66+
file=sys.stderr,
67+
)
68+
sys.exit(1)
69+
70+
1671
def create_app(
1772
base_dir: str | None = None,
1873
exclusion_rules_path: str | None = None,
@@ -60,9 +115,10 @@ def build_cli_parser() -> argparse.ArgumentParser:
60115

61116
if __name__ == "__main__":
62117
args = build_cli_parser().parse_args()
118+
validate_startup_cli(args)
63119

64120
app = create_app(base_dir=args.base_dir, exclusion_rules_path=args.exclude_rules)
65-
print(f"Claude Code Chat Browser running at http://{args.host}:{args.port}")
121+
print(f"Claude Code Chat Browser running at {format_listen_url(args.host, args.port)}")
66122
# Reloader follows --debug on Unix only (Werkzeug file watcher, not the interactive debugger).
67123
app.run(
68124
host=args.host,

docs/api-reference.md

Lines changed: 71 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,22 @@ HTTP API for **claude-code-chat-browser**. All `/api/*` routes return JSON unles
66

77
**Source of truth for error codes:** [`api/error_codes.py`](../api/error_codes.py)
88

9+
**Deprecation and removal:** See [`deprecation-policy.md`](deprecation-policy.md) for how stability labels are applied and fields are removed. Field labels are defined in [API field stability](#api-field-stability) below.
10+
11+
---
12+
13+
## API field stability
14+
15+
Each response field below is labeled:
16+
17+
| Label | Meaning |
18+
|-------|---------|
19+
| **stable** | Will not be renamed or removed without a documented deprecation period |
20+
| **experimental** | May change in any release; do not build long-lived integrations on these fields |
21+
| **deprecated** | Still returned; use the documented replacement; removal announced in [CHANGELOG](../CHANGELOG.md) |
22+
23+
**Migration:** Breaking changes use additive deprecation first (new field → deprecate old → remove after policy period). Versioned routes (e.g. `/api/v2/...`) are reserved for future breaking reshapes; none exist today.
24+
925
---
1026

1127
## Authentication
@@ -100,13 +116,13 @@ None.
100116

101117
`application/json` — array of project objects:
102118

103-
| Field | Type | Description |
104-
|-------|------|-------------|
105-
| `name` | string | Directory name under `~/.claude/projects/` (e.g. `F--boost-capy`) |
106-
| `path` | string | Absolute path to project directory |
107-
| `display_name` | string | Friendly name derived from session `cwd` when available |
108-
| `session_count` | integer | Count of titled sessions (updated in handler) |
109-
| `last_modified` | string (ISO 8601) | Latest message timestamp across titled sessions |
119+
| Field | Type | Stability | Description |
120+
|-------|------|-----------|-------------|
121+
| `name` | string | stable | Directory name under `~/.claude/projects/` (e.g. `F--boost-capy`) |
122+
| `path` | string | stable | Absolute path to project directory |
123+
| `display_name` | string | stable | Friendly name derived from session `cwd` when available |
124+
| `session_count` | integer | stable | Count of titled sessions (updated in handler) |
125+
| `last_modified` | string (ISO 8601) | stable | Latest message timestamp across titled sessions |
110126

111127
```json
112128
[
@@ -148,19 +164,19 @@ Lists sessions in one project with summary fields for the workspace sidebar. Ski
148164

149165
`application/json` — array of session row objects:
150166

151-
| Field | Type | Description |
152-
|-------|------|-------------|
153-
| `id` | string | Session id (filename without `.jsonl`) |
154-
| `path` | string | Absolute path to JSONL file |
155-
| `size_bytes` | integer | File size |
156-
| `modified` | number | File mtime (epoch seconds) |
157-
| `title` | string | Parsed session title |
158-
| `models` | string[] | Models used in session |
159-
| `tokens` | integer | Sum of input + output tokens |
160-
| `tool_calls` | integer | Total tool calls |
161-
| `first_timestamp` | string \| null | First message timestamp |
162-
| `last_timestamp` | string \| null | Last message timestamp |
163-
| `error` | boolean | Optional; `true` if parse failed (card shows error state) |
167+
| Field | Type | Stability | Description |
168+
|-------|------|-----------|-------------|
169+
| `id` | string | stable | Session id (filename without `.jsonl`) |
170+
| `path` | string | stable | Absolute path to JSONL file |
171+
| `size_bytes` | integer | stable | File size |
172+
| `modified` | number | stable | File mtime (epoch seconds) |
173+
| `title` | string | stable | Parsed session title |
174+
| `models` | string[] | stable | Models used in session |
175+
| `tokens` | integer | stable | Sum of input + output tokens |
176+
| `tool_calls` | integer | stable | Total tool calls |
177+
| `first_timestamp` | string \| null | stable | First message timestamp |
178+
| `last_timestamp` | string \| null | stable | Last message timestamp |
179+
| `error` | boolean | stable | Optional; `true` if parse failed (card shows error state) |
164180

165181
#### Errors
166182

@@ -191,14 +207,14 @@ Returns the full parsed session: title, metadata, and messages (including tool c
191207

192208
`application/json` — session object:
193209

194-
| Top-level field | Type | Description |
195-
|-----------------|------|-------------|
196-
| `session_id` | string | Session identifier |
197-
| `title` | string | Inferred title from first human message |
198-
| `messages` | array | Ordered message objects (`role`, `text`/`content`, tool fields, etc.) |
199-
| `metadata` | object | Tokens, models, timestamps, file activity, tool counts, `cwd`, `git_branch`, … |
210+
| Top-level field | Type | Stability | Description |
211+
|-----------------|------|-----------|-------------|
212+
| `session_id` | string | stable | Session identifier |
213+
| `title` | string | stable | Inferred title from first human message |
214+
| `messages` | array | stable | Ordered message objects (`role`, `text`/`content`, tool fields, etc.) |
215+
| `metadata` | object | stable | Tokens, models, timestamps, file activity, tool counts, `cwd`, `git_branch`, … |
200216

201-
See [`utils/jsonl_parser.py`](../utils/jsonl_parser.py) `parse_session()` for the full metadata shape.
217+
Nested keys inside `messages[]` and `metadata` follow the parser output; new parser fields may appear as **experimental** until listed here. See [`utils/jsonl_parser.py`](../utils/jsonl_parser.py) `parse_session()` for the full metadata shape.
202218

203219
#### Errors
204220

@@ -228,21 +244,21 @@ Same as session detail.
228244

229245
`application/json` — stats object from [`utils/session_stats.py`](../utils/session_stats.py) `compute_stats()`:
230246

231-
| Field | Type | Description |
232-
|-------|------|-------------|
233-
| `files_touched` | object | `read`, `written`, `created`, `total_unique` file lists |
234-
| `commands_run` | array | Bash commands with exit metadata |
235-
| `urls_accessed` | string[] | Web fetch URLs |
236-
| `conversation_turns` | integer | Human/assistant turn count |
237-
| `wall_clock_seconds` | number \| null | Session duration |
238-
| `wall_clock_display` | string \| null | Human-readable duration |
239-
| `cost_estimate_usd` | number | Best-effort USD estimate from token usage |
240-
| `tool_result_summary` | object | Aggregated tool result stats |
241-
| `stop_reason_summary` | object | Stop reason counts |
242-
| `entry_type_counts` | object | JSONL entry type counts |
243-
| `sidechain_message_count` | integer | Sidechain entries |
244-
| `api_error_count` | integer | API errors in session |
245-
| `compaction_events` | array | Context compaction markers |
247+
| Field | Type | Stability | Description |
248+
|-------|------|-----------|-------------|
249+
| `files_touched` | object | stable | `read`, `written`, `created`, `total_unique` file lists |
250+
| `commands_run` | array | stable | Bash commands with exit metadata |
251+
| `urls_accessed` | string[] | stable | Web fetch URLs |
252+
| `conversation_turns` | integer | stable | Human/assistant turn count |
253+
| `wall_clock_seconds` | number \| null | stable | Session duration |
254+
| `wall_clock_display` | string \| null | stable | Human-readable duration |
255+
| `cost_estimate_usd` | number | stable | Best-effort USD estimate from token usage |
256+
| `tool_result_summary` | object | stable | Aggregated tool result stats |
257+
| `stop_reason_summary` | object | stable | Stop reason counts |
258+
| `entry_type_counts` | object | stable | JSONL entry type counts |
259+
| `sidechain_message_count` | integer | stable | Sidechain entries |
260+
| `api_error_count` | integer | stable | API errors in session |
261+
| `compaction_events` | array | stable | Context compaction markers |
246262

247263
#### Errors
248264

@@ -276,14 +292,14 @@ Case-insensitive substring search across all non-excluded messages in all projec
276292

277293
`application/json` — array of hit objects:
278294

279-
| Field | Type | Description |
280-
|-------|------|-------------|
281-
| `project` | string | Project `name` |
282-
| `session_id` | string | Session id |
283-
| `title` | string | Session title |
284-
| `role` | string | Message role (`human`, `assistant`, …) |
285-
| `timestamp` | string \| null | Message timestamp |
286-
| `snippet` | string | ~160 chars around match |
295+
| Field | Type | Stability | Description |
296+
|-------|------|-----------|-------------|
297+
| `project` | string | stable | Project `name` |
298+
| `session_id` | string | stable | Session id |
299+
| `title` | string | stable | Session title |
300+
| `role` | string | stable | Message role (`human`, `assistant`, …) |
301+
| `timestamp` | string \| null | stable | Message timestamp |
302+
| `snippet` | string | experimental | ~160 chars around match; length may change |
287303

288304
#### Errors
289305

@@ -306,11 +322,11 @@ Read-only snapshot of bulk-export state persisted under `~/.claude-code-chat-bro
306322

307323
#### Response — `200 OK`
308324

309-
| Field | Type | Description |
310-
|-------|------|-------------|
311-
| `last_export_time` | string \| null | ISO timestamp of last completed bulk export |
312-
| `last_export_session_count` | integer | Sessions in last bulk export run |
313-
| `export_count` | integer | **Legacy alias** — same value as `last_export_session_count`; prefer `last_export_session_count` in new integrations (kept for SPA backwards compatibility) |
325+
| Field | Type | Stability | Description |
326+
|-------|------|-----------|-------------|
327+
| `last_export_time` | string \| null | stable | ISO timestamp of last completed bulk export |
328+
| `last_export_session_count` | integer | stable | Sessions in last bulk export run |
329+
| `export_count` | integer | deprecated | Legacy alias of `last_export_session_count`; prefer `last_export_session_count` in new code (still returned for SPA compatibility; removal per [deprecation-policy.md](deprecation-policy.md)) |
314330

315331
```json
316332
{

docs/deprecation-policy.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Deprecation policy
2+
3+
This document defines how **claude-code-chat-browser** evolves its HTTP JSON API and CLI without breaking integrators and the bundled SPA unexpectedly.
4+
5+
## Principles
6+
7+
1. **Documented fields are a contract.** See [API reference](api-reference.md) — each field is marked `stable`, `experimental`, or `deprecated`.
8+
2. **Additive first.** Prefer adding a new field over renaming an existing one.
9+
3. **Deprecate before removing.** A deprecated field remains in responses for at least **one release** after the deprecation is announced in [CHANGELOG](../CHANGELOG.md) and the API reference. Fields still read by the bundled SPA need **at least two releases** — see [Removal criteria](#removal-criteria) below.
10+
4. **SPA and scripts.** Update `static/js/*.js` and any internal callers before removing a field.
11+
12+
## How we announce deprecation
13+
14+
| Channel | What to update |
15+
|---------|----------------|
16+
| CHANGELOG | `### Deprecated` under the release that announces the change |
17+
| API reference | Set field stability to `deprecated` with a short note and replacement |
18+
| Response (optional) | Future: `Deprecation` header or JSON `_deprecated` map — not required today |
19+
20+
## Removal criteria
21+
22+
A deprecated field may be removed when:
23+
24+
- At least one release has shipped with the field still present but documented as deprecated, and
25+
- The bundled SPA no longer reads the field, and
26+
- Tests and CHANGELOG document the removal.
27+
28+
For fields actively read by the bundled SPA (which does not track an external API version), removal happens no earlier than **two tagged releases** after the release that documented the deprecation in CHANGELOG (for example, deprecated in `0.1.0`, removable from `0.3.0` at earliest when versions advance `0.1.0``0.2.0``0.3.0`), and no earlier than **14 calendar days** after that deprecation announcement.
29+
30+
## Example (in progress)
31+
32+
| Field | Endpoint | Status | Replacement |
33+
|-------|----------|--------|-------------|
34+
| `export_count` | `GET /api/export/state` | deprecated | `last_export_session_count` |
35+
36+
## Versioning
37+
38+
Release versions follow `MAJOR.MINOR.PATCH` in `app.__version__` and [CHANGELOG](../CHANGELOG.md). Until the first git tag ships, `main` may carry a `.dev0` suffix (for example `0.1.0.dev0`); the CHANGELOG `[Unreleased]` section is the source of truth for what is not yet tagged.
39+
40+
| Bump | Pre-1.0 meaning |
41+
|------|-----------------|
42+
| **Patch** | Bug fixes and documentation; no intentional API removals |
43+
| **Minor** | Additive API/features; deprecations may be announced |
44+
| **Major** | Reserved for the `1.0.0` line and later: signals a stable HTTP JSON contract for external integrators and may include breaking removals that completed their deprecation period |
45+
46+
While the project is pre-1.0, treat **minor** bumps as the usual vehicle for deprecations and **patch** bumps for safe fixes.

0 commit comments

Comments
 (0)