-
Notifications
You must be signed in to change notification settings - Fork 5.5k
Expand file tree
/
Copy pathpaths.py
More file actions
472 lines (330 loc) · 14.7 KB
/
Copy pathpaths.py
File metadata and controls
472 lines (330 loc) · 14.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
"""Canonical filesystem contract for Headroom.
This module defines the single source of truth for where Headroom reads and
writes files. It introduces two canonical roots:
* ``HEADROOM_CONFIG_DIR`` -- read-mostly configuration (defaults to
``~/.headroom/config``). Holds model catalogs, plugin settings, and other
configuration that users or admins edit.
* ``HEADROOM_WORKSPACE_DIR`` -- read-write state (defaults to ``~/.headroom``).
Holds runtime caches, telemetry outputs, logs, savings history, memory
databases, and anything else that the running proxy/CLI writes to.
Precedence for every per-resource helper is::
explicit argument > per-resource env var > derived from canonical root >
default
Adding the canonical root env vars is strictly additive: every existing
per-resource override (``HEADROOM_SAVINGS_PATH``, ``HEADROOM_TOIN_PATH``,
``HEADROOM_SUBSCRIPTION_STATE_PATH``, ``HEADROOM_MODEL_LIMITS``, ...)
continues to take precedence with identical semantics.
Implementation notes:
* Helpers return ``Path`` (never ``str``). Callers that need a string cast
at the callsite.
* Helpers are pure -- they never call ``mkdir``. Use the ``ensure_*``
variants when the caller needs the directory to exist.
* No caching. Every call re-reads the environment so that ``monkeypatch``
in tests works without extra hoops.
"""
from __future__ import annotations
import os
from pathlib import Path
# ---------------------------------------------------------------------------
# Canonical env var names
# ---------------------------------------------------------------------------
HEADROOM_CONFIG_DIR_ENV = "HEADROOM_CONFIG_DIR"
HEADROOM_WORKSPACE_DIR_ENV = "HEADROOM_WORKSPACE_DIR"
# ---------------------------------------------------------------------------
# Legacy per-resource env vars (kept for backward compatibility)
# ---------------------------------------------------------------------------
HEADROOM_SAVINGS_PATH_ENV = "HEADROOM_SAVINGS_PATH"
HEADROOM_SAVINGS_EVENTS_PATH_ENV = "HEADROOM_SAVINGS_EVENTS_PATH"
HEADROOM_TOIN_PATH_ENV = "HEADROOM_TOIN_PATH"
HEADROOM_SUBSCRIPTION_STATE_PATH_ENV = "HEADROOM_SUBSCRIPTION_STATE_PATH"
HEADROOM_SETTINGS_PATH_ENV = "HEADROOM_SETTINGS_PATH"
# ---------------------------------------------------------------------------
# Default sub-path fragments
# ---------------------------------------------------------------------------
_WORKSPACE_DIR_DEFAULT = ".headroom"
_CONFIG_DIR_DEFAULT_SUFFIX = "config"
# Resource file/sub-dir names (kept here so nothing else has to hardcode them)
_SAVINGS_FILE = "proxy_savings.json"
_SETTINGS_FILE = "settings.json"
_TOIN_FILE = "toin.json"
_MODELS_FILE = "models.json"
_SUBSCRIPTION_FILE = "subscription_state.json"
_MEMORY_DB_FILE = "memory.db"
_MEMORIES_DIR = "memories"
_LICENSE_CACHE_FILE = "license_cache.json"
_SESSION_STATS_FILE = "session_stats.jsonl"
_SAVINGS_EVENTS_FILE = "savings_events.jsonl"
_SYNC_STATE_FILE = "sync_state.json"
_BRIDGE_STATE_FILE = "bridge_state.json"
_LOGS_DIR = "logs"
# Legacy shared runtime-log filename. Kept only as the readers' backward-compat
# fallback; live proxies now write per-port files (see ``proxy_log_path``).
_PROXY_LOG_FILE = "proxy.log"
_PROXY_STDIO_LOG_FILE = "proxy-stdio.log"
_DEBUG_400_DIR = "debug_400"
_CODEX_WIRE_DEBUG_DIR = "codex_wire"
_BIN_DIR = "bin"
_PROXY_CLIENTS_DIR = "clients"
_DEPLOY_DIR = "deploy"
_PLUGINS_DIR = "plugins"
# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------
def _env(name: str) -> str:
"""Return a trimmed environment value, or ``""`` when unset/blank."""
return os.environ.get(name, "").strip()
# ---------------------------------------------------------------------------
# Process-wide stateless flag
# ---------------------------------------------------------------------------
# Stateless mode forbids writes to the workspace. Many persisters are
# module-level singletons reached without a config object, so the proxy records
# the mode here once at startup and writers consult ``process_is_stateless()``.
_PROCESS_STATELESS: bool = False
def set_process_stateless(value: bool) -> None:
"""Record process-wide stateless mode (set once at proxy startup)."""
global _PROCESS_STATELESS
_PROCESS_STATELESS = bool(value)
def process_is_stateless() -> bool:
"""True when the process must not write to the workspace.
True if ``set_process_stateless(True)`` was called OR the ``HEADROOM_STATELESS``
environment variable is set, so non-proxy entrypoints honor it too.
"""
if _PROCESS_STATELESS:
return True
return _env("HEADROOM_STATELESS").lower() in ("1", "true", "yes", "on")
def _resolve(explicit: str | os.PathLike[str] | None, env_var: str, derived: Path) -> Path:
"""Apply the standard precedence: explicit > env > derived.
``explicit`` and the env-var value are both passed through ``expanduser()``
so that callers can pass ``"~/foo/bar"`` and have it resolve naturally.
"""
if explicit is not None and str(explicit) != "":
return Path(explicit).expanduser()
env_value = _env(env_var)
if env_value:
return Path(env_value).expanduser()
return derived
# ---------------------------------------------------------------------------
# Canonical roots
# ---------------------------------------------------------------------------
def workspace_dir() -> Path:
"""Return the workspace (read-write state) root directory.
Resolution order:
1. ``$HEADROOM_WORKSPACE_DIR`` (trimmed, tilde-expanded) if set.
2. ``~/.headroom`` otherwise.
"""
env_value = _env(HEADROOM_WORKSPACE_DIR_ENV)
if env_value:
return Path(env_value).expanduser()
return Path.home() / _WORKSPACE_DIR_DEFAULT
def config_dir() -> Path:
"""Return the config (read-mostly) root directory.
Resolution order:
1. ``$HEADROOM_CONFIG_DIR`` (trimmed, tilde-expanded) if set.
2. ``$HEADROOM_WORKSPACE_DIR/config`` when the workspace env var is set
so that a single override relocates both roots coherently.
3. ``~/.headroom/config`` otherwise.
"""
env_value = _env(HEADROOM_CONFIG_DIR_ENV)
if env_value:
return Path(env_value).expanduser()
workspace_env = _env(HEADROOM_WORKSPACE_DIR_ENV)
if workspace_env:
return Path(workspace_env).expanduser() / _CONFIG_DIR_DEFAULT_SUFFIX
return Path.home() / _WORKSPACE_DIR_DEFAULT / _CONFIG_DIR_DEFAULT_SUFFIX
def ensure_workspace_dir() -> Path:
"""Return :func:`workspace_dir`, creating it if it does not yet exist."""
path = workspace_dir()
path.mkdir(parents=True, exist_ok=True)
return path
def ensure_config_dir() -> Path:
"""Return :func:`config_dir`, creating it if it does not yet exist."""
path = config_dir()
path.mkdir(parents=True, exist_ok=True)
return path
# ---------------------------------------------------------------------------
# Per-resource helpers -- workspace bucket
# ---------------------------------------------------------------------------
def savings_path(explicit: str | os.PathLike[str] | None = None) -> Path:
"""Return the path for the proxy savings JSON ledger."""
return _resolve(
explicit,
HEADROOM_SAVINGS_PATH_ENV,
workspace_dir() / _SAVINGS_FILE,
)
def settings_path(explicit: str | os.PathLike[str] | None = None) -> Path:
"""Return the path for the dashboard-managed settings JSON file."""
return _resolve(
explicit,
HEADROOM_SETTINGS_PATH_ENV,
workspace_dir() / _SETTINGS_FILE,
)
def toin_path(explicit: str | os.PathLike[str] | None = None) -> Path:
"""Return the path for the TOIN telemetry JSON file.
TOIN is classified as workspace state because it is actively written by
the running proxy (it's a compression feedback loop). The default stays
``~/.headroom/toin.json`` to preserve byte-for-byte backward compat.
"""
return _resolve(
explicit,
HEADROOM_TOIN_PATH_ENV,
workspace_dir() / _TOIN_FILE,
)
def subscription_state_path(explicit: str | os.PathLike[str] | None = None) -> Path:
"""Return the path for the subscription tracker state JSON."""
return _resolve(
explicit,
HEADROOM_SUBSCRIPTION_STATE_PATH_ENV,
workspace_dir() / _SUBSCRIPTION_FILE,
)
def memory_db_path() -> Path:
"""Return the default memory SQLite path."""
return workspace_dir() / _MEMORY_DB_FILE
def native_memory_dir() -> Path:
"""Return the default native-memory directory."""
return workspace_dir() / _MEMORIES_DIR
def license_cache_path() -> Path:
"""Return the path for the cached license envelope."""
return workspace_dir() / _LICENSE_CACHE_FILE
def session_stats_path() -> Path:
"""Return the path for the per-session stats JSONL file."""
return workspace_dir() / _SESSION_STATS_FILE
def savings_events_path(explicit: str | os.PathLike[str] | None = None) -> Path:
"""Return the path for the durable append-only savings event ledger.
Unlike :func:`session_stats_path` (pruned to a short rolling window), this
file accrues one line per compression across proxy restarts and concurrent
MCP processes, and is the source of truth for ``headroom savings``.
"""
return _resolve(
explicit,
HEADROOM_SAVINGS_EVENTS_PATH_ENV,
workspace_dir() / _SAVINGS_EVENTS_FILE,
)
def sync_state_path() -> Path:
"""Return the path for memory sync state."""
return workspace_dir() / _SYNC_STATE_FILE
def bridge_state_path() -> Path:
"""Return the path for the memory bridge state."""
return workspace_dir() / _BRIDGE_STATE_FILE
def log_dir() -> Path:
"""Return the directory for Headroom log files."""
return workspace_dir() / _LOGS_DIR
def proxy_log_path(port: int | None = None, *, process_id: int | None = None) -> Path:
"""Return the path for the proxy runtime log file.
Multi-worker processes pass both values and write
``proxy-<port>-<pid>.log``. Omitting *process_id* returns the standard
per-port name; omitting *port* returns the legacy shared name. Readers
honor all three.
"""
if port is None:
name = _PROXY_LOG_FILE
elif process_id is None:
name = f"proxy-{port}.log"
else:
name = f"proxy-{port}-{process_id}.log"
return log_dir() / name
def proxy_stdio_log_path(port: int | None = None) -> Path:
"""Return the path for the proxy stdout/stderr capture file.
Per-port for the same reason as :func:`proxy_log_path`; the legacy
``proxy-stdio.log`` name is used when *port* is omitted.
"""
name = f"proxy-stdio-{port}.log" if port is not None else _PROXY_STDIO_LOG_FILE
return log_dir() / name
def debug_400_dir() -> Path:
"""Return the directory used to stash HTTP 400 debug payloads."""
return log_dir() / _DEBUG_400_DIR
def codex_wire_debug_dir() -> Path:
"""Return the directory used for opt-in Codex wire debug captures."""
return log_dir() / _CODEX_WIRE_DEBUG_DIR
def bin_dir() -> Path:
"""Return the directory where Headroom ships vendored binaries."""
return workspace_dir() / _BIN_DIR
def proxy_clients_dir(port: int) -> Path:
"""Per-port dir of live wrap-client markers (one file per client PID)."""
return workspace_dir() / _PROXY_CLIENTS_DIR / str(port)
def deploy_root() -> Path:
"""Return the root directory for persistent deployment profiles."""
return workspace_dir() / _DEPLOY_DIR
def beacon_lock_path(port: int) -> Path:
"""Return the per-port proxy beacon lock file path."""
return workspace_dir() / f".beacon_lock_{int(port)}"
def proxy_start_lock_path(port: int) -> Path:
"""Return the per-port lock used to serialize wrap proxy startup."""
return workspace_dir() / f".proxy_start_{int(port)}.lock"
# ---------------------------------------------------------------------------
# Per-resource helpers -- config bucket
# ---------------------------------------------------------------------------
def models_config_path() -> Path:
"""Return the default path for the models catalog JSON.
Note: the ``HEADROOM_MODEL_LIMITS`` env var is a *content* override
(it can hold either a JSON string or a filesystem path) and is handled
by the provider layer. This helper only returns the default file
location and deliberately ignores ``HEADROOM_MODEL_LIMITS``.
"""
return config_dir() / _MODELS_FILE
# ---------------------------------------------------------------------------
# Plugin-author entry points
# ---------------------------------------------------------------------------
def _validate_plugin_name(plugin_name: str) -> None:
"""Reject plugin names that would escape the ``plugins/`` sandbox.
Path separators (``/``, ``\\``) are rejected so a name cannot address a
subdirectory. ``.`` and ``..`` are rejected because ``plugins / ".."``
resolves to the plugins-parent (i.e. the whole config/workspace root),
handing a plugin read/write access to every other plugin's state and the
workspace's savings ledger, memory DB, license cache, and logs. NUL is
rejected because it terminates paths on POSIX APIs.
"""
if (
not plugin_name
or plugin_name in {".", ".."}
or "/" in plugin_name
or "\\" in plugin_name
or "\x00" in plugin_name
):
raise ValueError(f"invalid plugin name: {plugin_name!r}")
def plugin_config_dir(plugin_name: str) -> Path:
"""Return the config directory for a named plugin."""
_validate_plugin_name(plugin_name)
return config_dir() / _PLUGINS_DIR / plugin_name
def plugin_workspace_dir(plugin_name: str) -> Path:
"""Return the workspace directory for a named plugin."""
_validate_plugin_name(plugin_name)
return workspace_dir() / _PLUGINS_DIR / plugin_name
__all__ = [
"HEADROOM_CONFIG_DIR_ENV",
"HEADROOM_WORKSPACE_DIR_ENV",
"HEADROOM_SAVINGS_PATH_ENV",
"HEADROOM_SAVINGS_EVENTS_PATH_ENV",
"HEADROOM_TOIN_PATH_ENV",
"HEADROOM_SUBSCRIPTION_STATE_PATH_ENV",
"HEADROOM_SETTINGS_PATH_ENV",
"set_process_stateless",
"process_is_stateless",
"config_dir",
"workspace_dir",
"ensure_config_dir",
"ensure_workspace_dir",
"savings_path",
"toin_path",
"subscription_state_path",
"memory_db_path",
"native_memory_dir",
"license_cache_path",
"session_stats_path",
"savings_events_path",
"settings_path",
"sync_state_path",
"bridge_state_path",
"log_dir",
"proxy_log_path",
"proxy_stdio_log_path",
"debug_400_dir",
"codex_wire_debug_dir",
"bin_dir",
"proxy_clients_dir",
"deploy_root",
"beacon_lock_path",
"proxy_start_lock_path",
"models_config_path",
"plugin_config_dir",
"plugin_workspace_dir",
]