Skip to content

Commit 0cfbee7

Browse files
Stop /health from returning local file paths (#103)
* fix(health): stop returning local file paths; answer runtime ownership instead /health exposed page_path and page_candidates, which reveal local paths to any local reader. The installers and the Windows start script used page_path to confirm they own the server, so they now ask GET /health?runtime=<root> and check a boolean runtime_matches; the path never leaves the server. Co-Authored-By: Claude Code <noreply@anthropic.com> * test(install): prove the ownership check rejects unconfirmed servers The fake health server answers runtime ownership and still carries page_path for fixtures that run the older main-branch installer. Co-Authored-By: Claude Code <noreply@anthropic.com> * fix(ci): check runtime ownership in the Windows update job; keep installer tests POSIX-only The update job read page_path from /health, which this branch removes; it now asks /health?runtime=. The new ownership test class had taken the POSIX skip decorator from InstallerModeTests, so those shell tests ran on Windows. Co-Authored-By: Claude Code <noreply@anthropic.com> --------- Co-authored-by: Claude Code <noreply@anthropic.com>
1 parent b4845de commit 0cfbee7

9 files changed

Lines changed: 170 additions & 52 deletions

File tree

‎.github/workflows/install-checks.yml‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -507,8 +507,9 @@ jobs:
507507
}
508508
$Health = Invoke-RestMethod -Uri "http://127.0.0.1:8722/health" -TimeoutSec 10
509509
if (-not $Health.ok) { throw "server is not healthy: $($Health | ConvertTo-Json -Compress)" }
510-
$ExpectedPage = [System.IO.Path]::GetFullPath((Join-Path $Runtime "page.html"))
511-
if ([System.IO.Path]::GetFullPath([string]$Health.page_path) -ne $ExpectedPage) {
510+
$OwnershipUri = "http://127.0.0.1:8722/health?runtime=" + [uri]::EscapeDataString([System.IO.Path]::GetFullPath($Runtime))
511+
$Ownership = Invoke-RestMethod -Uri $OwnershipUri -TimeoutSec 10
512+
if ($Ownership.runtime_matches -ne $true) {
512513
throw "a different runtime answers on port 8722"
513514
}
514515
$Status = Invoke-RestMethod -Uri "http://127.0.0.1:8722/updates/status" -TimeoutSec 10

‎scripts/install‎

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -371,20 +371,18 @@ fi
371371
[[ "$server_launch_state" == *"$INSTALL_ROOT/meter.py"* ]] \
372372
|| fail "a different Token Meter installation owns the local server LaunchAgent."
373373

374-
printf '%s' "$health_json" | python3 -c '
375-
import json, os, sys
376-
root = os.path.realpath(sys.argv[1])
377-
payload = json.load(sys.stdin)
378-
raw_page = payload.get("page_path")
379-
page = os.path.realpath(raw_page) if raw_page else ""
374+
# /health never returns local paths; it answers whether its page lives under this runtime.
375+
ownership_root="$(cd "$INSTALL_ROOT" && pwd -P)"
376+
ownership_json="$(curl -fsS --max-time 5 --get --data-urlencode "runtime=$ownership_root" "$HEALTH_URL" 2>/dev/null || true)"
377+
printf '%s' "$ownership_json" | python3 -c '
378+
import json, sys
380379
try:
381-
matches = bool(page) and os.path.commonpath([root, page]) == root
380+
payload = json.load(sys.stdin)
382381
except ValueError:
383-
matches = False
384-
if not matches:
385-
raise SystemExit("Token Meter installation failed: a different server owns port 8722 "
386-
f"(page_path={page or 'unknown'}).")
387-
' "$INSTALL_ROOT"
382+
payload = {}
383+
if payload.get("runtime_matches") is not True:
384+
raise SystemExit("Token Meter installation failed: a different server owns port 8722.")
385+
'
388386

389387
if (( BACKEND_ONLY )); then
390388
printf '%s\n' "backend-only" > "$INSTALL_ROOT/INSTALL_MODE"

‎scripts/install-linux‎

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -272,20 +272,18 @@ server_fragment="$(systemctl --user show token-meter-server.service -p FragmentP
272272
[[ "$server_fragment" == "${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user/token-meter-server.service" ]] \
273273
|| fail "a different unit owns token-meter-server.service."
274274

275-
printf '%s' "$health_json" | python3 -c '
276-
import json, os, sys
277-
root = os.path.realpath(sys.argv[1])
278-
payload = json.load(sys.stdin)
279-
raw_page = payload.get("page_path")
280-
page = os.path.realpath(raw_page) if raw_page else ""
275+
# /health never returns local paths; it answers whether its page lives under this runtime.
276+
ownership_root="$(cd "$INSTALL_ROOT" && pwd -P)"
277+
ownership_json="$(curl -fsS --max-time 5 --get --data-urlencode "runtime=$ownership_root" "$HEALTH_URL" 2>/dev/null || true)"
278+
printf '%s' "$ownership_json" | python3 -c '
279+
import json, sys
281280
try:
282-
matches = bool(page) and os.path.commonpath([root, page]) == root
281+
payload = json.load(sys.stdin)
283282
except ValueError:
284-
matches = False
285-
if not matches:
286-
raise SystemExit("Token Meter installation failed: a different server owns port 8722 "
287-
f"(page_path={page or 'unknown'}).")
288-
' "$INSTALL_ROOT"
283+
payload = {}
284+
if payload.get("runtime_matches") is not True:
285+
raise SystemExit("Token Meter installation failed: a different server owns port 8722.")
286+
'
289287

290288
if (( BACKEND_ONLY )); then
291289
printf '%s\n' "backend-only" > "$INSTALL_ROOT/INSTALL_MODE"

‎scripts/install-windows.ps1‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -578,9 +578,10 @@ try {
578578
if (-not $Health.ok -or -not $Health.state_ready) {
579579
Fail "the local server did not become ready."
580580
}
581-
$ExpectedPage = [System.IO.Path]::GetFullPath((Join-Path $InstallRoot "page.html"))
582-
$ActualPage = [System.IO.Path]::GetFullPath([string]$Health.page_path)
583-
if ($ActualPage -ne $ExpectedPage) {
581+
# /health never returns local paths; it answers whether its page lives under this runtime.
582+
$OwnershipUri = "http://127.0.0.1:8722/health?runtime=" + [uri]::EscapeDataString($InstallRoot)
583+
$Ownership = Invoke-RestMethod -Uri $OwnershipUri -TimeoutSec 5
584+
if (-not $Ownership.PSObject.Properties["runtime_matches"] -or $Ownership.runtime_matches -ne $true) {
584585
Fail "a different Token Meter installation owns port 8722."
585586
}
586587
if (-not $BackendOnly) {

‎scripts/start-token-meter.ps1‎

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,25 @@ param(
66
$ErrorActionPreference = "Stop"
77
Set-StrictMode -Version Latest
88

9-
function Get-Health {
9+
function Get-Health([string]$Runtime) {
10+
# /health never returns local paths; it answers whether its page lives under this runtime.
11+
$Uri = "http://127.0.0.1:8722/health?runtime=" + [uri]::EscapeDataString($Runtime)
1012
try {
11-
return Invoke-RestMethod -Uri "http://127.0.0.1:8722/health" -TimeoutSec 3
13+
return Invoke-RestMethod -Uri $Uri -TimeoutSec 3
1214
} catch {
1315
return $null
1416
}
1517
}
1618

17-
function Test-OwnedHealth($Health, [string]$ExpectedPage) {
18-
if (-not $Health -or -not $Health.page_path) {
19-
return $false
20-
}
21-
try {
22-
return [System.IO.Path]::GetFullPath([string]$Health.page_path) -eq $ExpectedPage
23-
} catch {
19+
function Test-OwnedHealth($Health) {
20+
if (-not $Health -or -not $Health.PSObject.Properties["runtime_matches"]) {
2421
return $false
2522
}
23+
return $Health.runtime_matches -eq $true
2624
}
2725

2826
$RuntimeRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot ".."))
2927
$MeterPath = [System.IO.Path]::GetFullPath((Join-Path $RuntimeRoot "meter.py"))
30-
$ExpectedPage = [System.IO.Path]::GetFullPath((Join-Path $RuntimeRoot "page.html"))
3128
$PythonRecord = Join-Path $RuntimeRoot "PYTHON_EXECUTABLE"
3229
$WindowedPythonRecord = Join-Path $RuntimeRoot "PYTHON_WINDOWED_EXECUTABLE"
3330
$PidPath = Join-Path $RuntimeRoot "meter.pid"
@@ -66,8 +63,8 @@ if (-not (Test-Path -LiteralPath $PythonExe -PathType Leaf)) {
6663
throw "Token Meter's configured Python executable is unavailable. Reinstall Token Meter."
6764
}
6865

69-
$Health = Get-Health
70-
if ($Health -and -not (Test-OwnedHealth $Health $ExpectedPage)) {
66+
$Health = Get-Health $RuntimeRoot
67+
if ($Health -and -not (Test-OwnedHealth $Health)) {
7168
throw "A different server owns http://127.0.0.1:8722."
7269
}
7370

@@ -110,8 +107,8 @@ if (-not $Health) {
110107
$Deadline = [DateTime]::UtcNow.AddSeconds($ReadinessTimeoutSeconds)
111108
$Ready = $false
112109
do {
113-
$Health = Get-Health
114-
if ($Health -and -not (Test-OwnedHealth $Health $ExpectedPage)) {
110+
$Health = Get-Health $RuntimeRoot
111+
if ($Health -and -not (Test-OwnedHealth $Health)) {
115112
throw "A different server owns http://127.0.0.1:8722."
116113
}
117114
if ($Health -and $Health.ok -and $Health.state_ready) {

‎tests/contracts/test_windows_packaging.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,12 @@ def test_windows_lifecycle_is_per_user_owned_and_local_only(self):
295295
self.assertIn(marker, installer)
296296
self.assertIn("http://127.0.0.1:8722/health", starter)
297297
self.assertIn("Test-OwnedHealth", starter)
298+
# Ownership is asked as a yes/no question; /health no longer returns local paths.
299+
for script in (starter, installer):
300+
self.assertIn("health?runtime=", script)
301+
self.assertIn("[uri]::EscapeDataString(", script)
302+
self.assertIn("runtime_matches", script)
303+
self.assertNotIn("page_path", script)
298304
self.assertIn("Remove-ItemProperty", uninstaller)
299305
self.assertIn("belongs to a different", uninstaller)
300306
self.assertNotIn("sudo", installer.lower())

‎tests/test_installer_modes.py‎

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import json
12
import os
23
import signal
34
import subprocess
5+
import sys
46
import tempfile
57
import textwrap
68
import time
@@ -146,6 +148,41 @@ def test_runner_reuses_compatible_build_until_source_changes(self):
146148
self.assertNotEqual(stale.returncode, 0)
147149

148150

151+
@unittest.skipUnless(os.name == "posix", "POSIX shell integration")
152+
class InstallerOwnershipCheckTests(unittest.TestCase):
153+
def test_unix_installers_ask_health_whether_they_own_the_server(self):
154+
root = Path(__file__).resolve().parents[1]
155+
for name in ("install", "install-linux"):
156+
script = (root / "scripts" / name).read_text(encoding="utf-8")
157+
with self.subTest(script=name):
158+
self.assertIn('ownership_root="$(cd "$INSTALL_ROOT" && pwd -P)"', script)
159+
self.assertIn('--data-urlencode "runtime=$ownership_root"', script)
160+
self.assertIn('payload.get("runtime_matches") is not True', script)
161+
self.assertNotIn("page_path", script)
162+
163+
def test_unix_ownership_check_rejects_servers_that_do_not_confirm_the_runtime(self):
164+
root = Path(__file__).resolve().parents[1]
165+
for name in ("install", "install-linux"):
166+
script = (root / "scripts" / name).read_text(encoding="utf-8")
167+
start = script.index('printf \'%s\' "$ownership_json" | python3 -c \'') + len('printf \'%s\' "$ownership_json" | python3 -c \'')
168+
check = script[start:script.index("\n'\n", start)]
169+
for answer, owned in (
170+
({"ok": True, "runtime_matches": True}, True),
171+
({"ok": True, "runtime_matches": False}, False),
172+
({"ok": True, "page_path": "/elsewhere/page.html"}, False),
173+
("not json", False),
174+
):
175+
with self.subTest(script=name, answer=answer):
176+
result = subprocess.run(
177+
[sys.executable, "-I", "-c", check],
178+
input=answer if isinstance(answer, str) else json.dumps(answer),
179+
capture_output=True, text=True,
180+
)
181+
self.assertEqual(result.returncode == 0, owned, result.stderr)
182+
if not owned:
183+
self.assertIn("a different server owns port 8722", result.stderr)
184+
185+
149186
@unittest.skipUnless(os.name == "posix", "POSIX shell integration")
150187
class InstallerModeTests(unittest.TestCase):
151188
def write_executable(self, path, source):
@@ -236,7 +273,8 @@ def installer_environment(self, workspace, platform):
236273
"""
237274
#!/usr/bin/env bash
238275
if [[ "${*: -1}" == */health ]]; then
239-
printf '{"ok": true, "state_ready": true, "page_path": "%s/page.html"}\n' "$TEST_INSTALL_ROOT"
276+
owned=false; [[ "$*" == *"runtime=$(cd "$TEST_INSTALL_ROOT" 2>/dev/null && pwd -P)"* ]] && owned=true
277+
printf '{"ok": true, "state_ready": true, "runtime_matches": %s, "page_path": "%s/page.html"}\n' "$owned" "$TEST_INSTALL_ROOT"
240278
else
241279
printf '{"ok": true}\n'
242280
fi
@@ -401,7 +439,8 @@ def test_installers_draw_and_clear_a_spinner_on_a_terminal(self):
401439
count=$(( $(cat {str(polls)!r} 2>/dev/null || echo 0) + 1 ))
402440
echo "$count" > {str(polls)!r}
403441
if (( count < 3 )); then exit 7; fi
404-
printf '{{"ok": true, "state_ready": true, "page_path": "%s/page.html"}}\n' "$TEST_INSTALL_ROOT"
442+
owned=false; [[ "$*" == *"runtime=$(cd "$TEST_INSTALL_ROOT" 2>/dev/null && pwd -P)"* ]] && owned=true
443+
printf '{{"ok": true, "state_ready": true, "runtime_matches": %s, "page_path": "%s/page.html"}}\n' "$owned" "$TEST_INSTALL_ROOT"
405444
else
406445
printf '{{"ok": true}}\n'
407446
fi
@@ -522,7 +561,8 @@ def test_menu_bar_builds_while_the_server_indexes(self):
522561
#!/usr/bin/env bash
523562
if [[ "${{*: -1}}" == */health ]]; then
524563
[[ -e {str(built)!r} ]] || exit 7
525-
printf '{{"ok": true, "state_ready": true, "page_path": "%s/page.html"}}\n' "$TEST_INSTALL_ROOT"
564+
owned=false; [[ "$*" == *"runtime=$(cd "$TEST_INSTALL_ROOT" 2>/dev/null && pwd -P)"* ]] && owned=true
565+
printf '{{"ok": true, "state_ready": true, "runtime_matches": %s, "page_path": "%s/page.html"}}\n' "$owned" "$TEST_INSTALL_ROOT"
526566
else
527567
printf '{{"ok": true}}\n'
528568
fi
@@ -574,7 +614,8 @@ def test_missing_toolchain_never_runs_swiftc_before_the_server_is_ready(self):
574614
#!/usr/bin/env bash
575615
if [[ "${{*: -1}}" == */health ]]; then
576616
touch {str(ready)!r}
577-
printf '{{"ok": true, "state_ready": true, "page_path": "%s/page.html"}}\n' "$TEST_INSTALL_ROOT"
617+
owned=false; [[ "$*" == *"runtime=$(cd "$TEST_INSTALL_ROOT" 2>/dev/null && pwd -P)"* ]] && owned=true
618+
printf '{{"ok": true, "state_ready": true, "runtime_matches": %s, "page_path": "%s/page.html"}}\n' "$owned" "$TEST_INSTALL_ROOT"
578619
else
579620
printf '{{"ok": true}}\n'
580621
fi

‎tests/test_meter.py‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15154,6 +15154,63 @@ def test_health_uses_cached_inventory_without_discovering_sessions(self):
1515415154
self.assertEqual(payload["sources"], 2400)
1515515155
self.assertEqual(payload["source_clients"], {"codex": 2300, "claude_code": 100})
1515615156

15157+
def test_health_never_exposes_local_paths(self):
15158+
page = os.path.join(os.path.expanduser("~"), "Library", "Token Meter", "runtime", "page.html")
15159+
candidates = [page, "/opt/token-meter/page.html"]
15160+
for found in (page, None):
15161+
with self.subTest(page_found=bool(found)), \
15162+
mock.patch.object(meter, "STATE", {}), \
15163+
mock.patch.object(meter, "page_path", return_value=found), \
15164+
mock.patch.object(meter, "PAGE_CANDIDATES", candidates):
15165+
payload, status = meter.health_state()
15166+
self.assertNotIn("page_path", payload)
15167+
self.assertNotIn("page_candidates", payload)
15168+
text = json.dumps(payload)
15169+
for fragment in (os.path.expanduser("~"), "page.html", "/opt/token-meter"):
15170+
self.assertNotIn(fragment, text)
15171+
self.assertEqual(payload["page_ready"], bool(found))
15172+
self.assertEqual(status, 200 if found else 503)
15173+
15174+
def test_health_answers_runtime_ownership_without_revealing_the_path(self):
15175+
with tempfile.TemporaryDirectory() as directory:
15176+
runtime = os.path.join(directory, "runtime")
15177+
os.makedirs(runtime)
15178+
page = os.path.join(runtime, "page.html")
15179+
cases = (
15180+
(runtime, page, True),
15181+
(runtime + os.sep, page, True),
15182+
(os.path.join(directory, "other"), page, False),
15183+
(runtime + "-old", page, False),
15184+
(runtime, None, False),
15185+
("relative/runtime", page, False),
15186+
("x" * 5000, page, False),
15187+
)
15188+
for expected, found, owned in cases:
15189+
with self.subTest(expected=expected[:40], found=bool(found)), \
15190+
mock.patch.object(meter, "STATE", {}), \
15191+
mock.patch.object(meter, "page_path", return_value=found):
15192+
payload, _ = meter.health_state(expected_runtime=expected)
15193+
self.assertIs(payload["runtime_matches"], owned)
15194+
self.assertNotIn(directory, json.dumps(payload))
15195+
with mock.patch.object(meter, "STATE", {}), \
15196+
mock.patch.object(meter, "page_path", return_value=page):
15197+
self.assertNotIn("runtime_matches", meter.health_state()[0])
15198+
15199+
def test_health_route_reads_one_runtime_query_value(self):
15200+
def get(query):
15201+
handler = object.__new__(meter.H)
15202+
handler.path = "/health" + query
15203+
handler.headers = {"Host": "127.0.0.1:8722"}
15204+
handler._send = mock.Mock()
15205+
handler.send_error = mock.Mock()
15206+
with mock.patch.object(meter, "health_state", return_value=({"ok": True}, 200)) as health:
15207+
handler.do_GET()
15208+
return health.call_args
15209+
15210+
self.assertEqual(get(""), mock.call(expected_runtime=None))
15211+
self.assertEqual(get("?runtime=%2Fopt%2Ftm"), mock.call(expected_runtime="/opt/tm"))
15212+
self.assertEqual(get("?runtime=a&runtime=b"), mock.call(expected_runtime=""))
15213+
1515715214
def test_health_marks_undiscovered_inventory_unavailable_instead_of_zero(self):
1515815215
inventory = {
1515915216
"ready": False, "sources": (), "count": None, "clients": {}, "updated_at": None,

‎token_meter/app.py‎

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13047,8 +13047,25 @@ def missing_performance_html():
1304713047
</html>"""
1304813048

1304913049

13050-
def health_state():
13051-
"""Return constant-time liveness/readiness from watcher-owned cached state."""
13050+
def _page_within_runtime(page, runtime):
13051+
"""True when the served page lives under ``runtime``; installers ask this instead of reading a path."""
13052+
if not page or not isinstance(runtime, str) or not runtime or len(runtime) > 4096:
13053+
return False
13054+
if not os.path.isabs(runtime):
13055+
return False
13056+
try:
13057+
root = os.path.normcase(os.path.realpath(runtime))
13058+
served = os.path.normcase(os.path.realpath(page))
13059+
return os.path.commonpath([root, served]) == root
13060+
except (OSError, ValueError):
13061+
return False
13062+
13063+
13064+
def health_state(expected_runtime=None):
13065+
"""Return constant-time liveness/readiness from watcher-owned cached state.
13066+
13067+
Local paths are never returned; ``expected_runtime`` lets an installer confirm ownership.
13068+
"""
1305213069
path = page_path()
1305313070
inventory = _SOURCE_INVENTORY
1305413071
inventory_ready = bool(inventory.get("ready"))
@@ -13063,9 +13080,9 @@ def health_state():
1306313080
"pricing_revision": served_pricing_revision(),
1306413081
"pricing_recalculating": pricing_recalculation_running(),
1306513082
"page_ready": bool(path),
13066-
"page_path": path,
13067-
"page_candidates": PAGE_CANDIDATES,
1306813083
}
13084+
if expected_runtime is not None:
13085+
payload["runtime_matches"] = _page_within_runtime(path, expected_runtime)
1306913086
return payload, 200 if path else 503
1307013087

1307113088

@@ -13631,7 +13648,9 @@ def do_GET(self):
1363113648
elif req_path in ("/api/v1/cost-discounts", "/api/v1/cost-discounts/resolve", "/api/v1/budgets"):
1363213649
self._api_get(parsed)
1363313650
elif req_path == "/health":
13634-
payload, status = health_state()
13651+
runtime_values = parse_qs(parsed.query, keep_blank_values=True).get("runtime")
13652+
expected = None if runtime_values is None else (runtime_values[0] if len(runtime_values) == 1 else "")
13653+
payload, status = health_state(expected_runtime=expected)
1363513654
self._send(json.dumps(payload), "application/json", status=status)
1363613655
elif req_path == "/updates/status":
1363713656
self._send(json.dumps(software_update_status()), "application/json")

0 commit comments

Comments
 (0)