Skip to content

Commit a5b3015

Browse files
feat: enforce debug/host guard and document API versioning
1 parent eb08e28 commit a5b3015

3 files changed

Lines changed: 46 additions & 18 deletions

File tree

app.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,18 @@ def is_loopback_host(host: str) -> bool:
2929
return False
3030

3131

32+
def format_listen_url(host: str, port: int) -> str:
33+
"""Return a valid ``http://`` URL for the startup banner (IPv6 hosts bracketed)."""
34+
h = (host or "").strip()
35+
if h.startswith("[") and h.endswith("]"):
36+
display_host = h
37+
elif ":" in h:
38+
display_host = f"[{h}]"
39+
else:
40+
display_host = h
41+
return f"http://{display_host}:{port}"
42+
43+
3244
def validate_startup_cli(args: argparse.Namespace) -> None:
3345
"""Refuse ``--debug`` when ``--host`` is reachable off loopback."""
3446
if args.debug and not is_loopback_host(args.host):
@@ -92,7 +104,7 @@ def build_cli_parser() -> argparse.ArgumentParser:
92104
validate_startup_cli(args)
93105

94106
app = create_app(base_dir=args.base_dir, exclusion_rules_path=args.exclude_rules)
95-
print(f"Claude Code Chat Browser running at http://{args.host}:{args.port}")
107+
print(f"Claude Code Chat Browser running at {format_listen_url(args.host, args.port)}")
96108
# Reloader follows --debug on Unix only (Werkzeug file watcher, not the interactive debugger).
97109
app.run(
98110
host=args.host,

docs/api-reference.md

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -244,21 +244,21 @@ Same as session detail.
244244

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

247-
| Field | Type | Description |
248-
|-------|------|-------------|
249-
| `files_touched` | object | `read`, `written`, `created`, `total_unique` file lists |
250-
| `commands_run` | array | Bash commands with exit metadata |
251-
| `urls_accessed` | string[] | Web fetch URLs |
252-
| `conversation_turns` | integer | Human/assistant turn count |
253-
| `wall_clock_seconds` | number \| null | Session duration |
254-
| `wall_clock_display` | string \| null | Human-readable duration |
255-
| `cost_estimate_usd` | number | Best-effort USD estimate from token usage |
256-
| `tool_result_summary` | object | Aggregated tool result stats |
257-
| `stop_reason_summary` | object | Stop reason counts |
258-
| `entry_type_counts` | object | JSONL entry type counts |
259-
| `sidechain_message_count` | integer | Sidechain entries |
260-
| `api_error_count` | integer | API errors in session |
261-
| `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 |
262262

263263
#### Errors
264264

tests/test_cli_args.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
2222
sys.path.insert(0, REPO_ROOT)
2323

24-
from app import build_cli_parser, is_loopback_host, validate_startup_cli
24+
from app import build_cli_parser, format_listen_url, is_loopback_host, validate_startup_cli
2525
from scripts.export import build_parser
2626

2727

@@ -334,12 +334,28 @@ def test_validate_startup_cli_allows_loopback_debug(self) -> None:
334334
args = parser.parse_args(["--host", "127.0.0.1", "--debug"])
335335
validate_startup_cli(args)
336336

337-
def test_validate_startup_cli_rejects_non_loopback_debug(self) -> None:
337+
def test_validate_startup_cli_rejects_non_loopback_debug(
338+
self, capsys: pytest.CaptureFixture[str]
339+
) -> None:
338340
parser = build_cli_parser()
339341
args = parser.parse_args(["--host", "0.0.0.0", "--debug"])
340342
with pytest.raises(SystemExit) as exc_info:
341343
validate_startup_cli(args)
342344
assert exc_info.value.code == 1
345+
err = capsys.readouterr().err
346+
assert "debug" in err.lower()
347+
assert "loopback" in err.lower()
348+
349+
@pytest.mark.parametrize(
350+
("host", "port", "expected"),
351+
[
352+
("127.0.0.1", 5000, "http://127.0.0.1:5000"),
353+
("::1", 8080, "http://[::1]:8080"),
354+
("[::1]", 8080, "http://[::1]:8080"),
355+
],
356+
)
357+
def test_format_listen_url(self, host: str, port: int, expected: str) -> None:
358+
assert format_listen_url(host, port) == expected
343359

344360
def test_validate_startup_cli_allows_non_loopback_without_debug(self) -> None:
345361
parser = build_cli_parser()

0 commit comments

Comments
 (0)