0ef5fcb1c5
Security / Dependency audit (pip-audit) (push) Has been cancelled
Security / CodeQL (javascript-typescript) (push) Has been cancelled
Security / CodeQL (python) (push) Has been cancelled
Security / Secret scan (gitleaks) (push) Has been cancelled
rust / test (ubuntu) (push) Has been cancelled
rust / simulator e2e (macos-latest) (push) Has been cancelled
rust / simulator e2e (ubuntu-latest) (push) Has been cancelled
rust / simulator e2e (windows-latest) (push) Has been cancelled
rust / wheels (aarch64-apple-darwin) (push) Has been cancelled
rust / wheels (x86_64-unknown-linux-gnu) (push) Has been cancelled
rust / wheels (x86_64-apple-darwin) (push) Has been cancelled
rust / audit (push) Has been cancelled
rust / parity (nightly, allowed to fail during Phase 0) (push) Has been cancelled
CI / commitlint (push) Has been skipped
Dev Containers / validate (.devcontainer/devcontainer.json, default) (push) Failing after 0s
Dev Containers / validate (.devcontainer/memory-stack/devcontainer.json, memory-stack) (push) Failing after 0s
Dev Containers / validate-worktree (push) Failing after 0s
CI / changes (push) Failing after 4s
Deploy Documentation / validate (push) Has been skipped
Deploy Documentation / deploy (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, claude) (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, codex) (push) Failing after 1s
Install Native E2E / install-native (ubuntu-latest) (push) Failing after 1s
OpenCode Plugin / typecheck + build + test (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, copilot) (push) Failing after 1s
Release Please / release-please (push) Failing after 1s
Wrap E2E / docker-wrap-e2e (push) Failing after 1s
Wrap Native E2E / wrap-native (ubuntu-latest) (push) Failing after 1s
Init E2E / docker-init-e2e (push) Failing after 4s
Merge Conflicts / merge-conflicts (push) Failing after 4s
CI / lint (push) Has been cancelled
CI / build-wheel (push) Has been cancelled
CI / build-wheel-windows (push) Has been cancelled
CI / prefetch-model (push) Has been cancelled
CI / test-dashboard-ui (push) Has been cancelled
CI / test (1) (push) Has been cancelled
CI / test (2) (push) Has been cancelled
CI / test (3) (push) Has been cancelled
CI / test (4) (push) Has been cancelled
CI / test-extras (push) Has been cancelled
CI / test-agno (push) Has been cancelled
CI / build (push) Has been cancelled
CI / workflow-validation (push) Has been cancelled
CI / docker-native-e2e (push) Has been cancelled
CI / windows-native-wrapper (push) Has been cancelled
CI / macos-native-wrapper (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / promote-latest (push) Has been cancelled
Init Native E2E / init-native (macos-latest, claude) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, codex) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, copilot) (push) Has been cancelled
Install Native E2E / install-native (macos-latest) (push) Has been cancelled
Wrap Native E2E / wrap-native (macos-latest) (push) Has been cancelled
426 lines
13 KiB
Python
426 lines
13 KiB
Python
"""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"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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"
|
|
_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"
|
|
_PROXY_LOG_FILE = "proxy.log"
|
|
_DEBUG_400_DIR = "debug_400"
|
|
_CODEX_WIRE_DEBUG_DIR = "codex_wire"
|
|
_BIN_DIR = "bin"
|
|
_PROXY_CLIENTS_DIR = "clients"
|
|
_RTK_UNIX = "rtk"
|
|
_RTK_WIN = "rtk.exe"
|
|
_LEAN_CTX_UNIX = "lean-ctx"
|
|
_LEAN_CTX_WIN = "lean-ctx.exe"
|
|
_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 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() -> Path:
|
|
"""Return the path for the proxy log file."""
|
|
|
|
return log_dir() / _PROXY_LOG_FILE
|
|
|
|
|
|
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 rtk_path() -> Path:
|
|
"""Return the path to the vendored ``rtk`` binary."""
|
|
|
|
name = _RTK_WIN if os.name == "nt" else _RTK_UNIX
|
|
return bin_dir() / name
|
|
|
|
|
|
def lean_ctx_path() -> Path:
|
|
"""Return the path to the vendored ``lean-ctx`` binary."""
|
|
|
|
name = _LEAN_CTX_WIN if os.name == "nt" else _LEAN_CTX_UNIX
|
|
return bin_dir() / name
|
|
|
|
|
|
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)}"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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 plugin_config_dir(plugin_name: str) -> Path:
|
|
"""Return the config directory for a named plugin."""
|
|
|
|
if not plugin_name or "/" in plugin_name or "\\" in plugin_name:
|
|
raise ValueError(f"invalid plugin name: {plugin_name!r}")
|
|
return config_dir() / _PLUGINS_DIR / plugin_name
|
|
|
|
|
|
def plugin_workspace_dir(plugin_name: str) -> Path:
|
|
"""Return the workspace directory for a named plugin."""
|
|
|
|
if not plugin_name or "/" in plugin_name or "\\" in plugin_name:
|
|
raise ValueError(f"invalid plugin name: {plugin_name!r}")
|
|
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",
|
|
"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",
|
|
"sync_state_path",
|
|
"bridge_state_path",
|
|
"log_dir",
|
|
"proxy_log_path",
|
|
"debug_400_dir",
|
|
"codex_wire_debug_dir",
|
|
"bin_dir",
|
|
"proxy_clients_dir",
|
|
"rtk_path",
|
|
"lean_ctx_path",
|
|
"deploy_root",
|
|
"beacon_lock_path",
|
|
"models_config_path",
|
|
"plugin_config_dir",
|
|
"plugin_workspace_dir",
|
|
]
|