Files
strukto-ai--mirage/python/mirage/cli/settings.py
T
wehub-resource-sync bcbd1bdb22
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:44 +08:00

293 lines
9.7 KiB
Python

# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import os
import tomllib
from dataclasses import dataclass
from pathlib import Path
from mirage.cli.env import ENV_DAEMON_URL, ENV_TOKEN
from mirage.server.auth import storage as auth_storage
from mirage.server.auth.config import (ENV_AUTH_MODE, ENV_JWT_ALG,
ENV_JWT_AUDIENCE,
ENV_JWT_AUTHORIZED_PARTIES,
ENV_JWT_CLOCK_SKEW, ENV_JWT_ISSUER,
ENV_JWT_PUBKEY_FILE)
from mirage.server.daemon_config import (ALLOWED_KEYS, NUMERIC_KEYS,
DaemonConfigError, read_daemon_table)
from mirage.server.env import (ENV_ALLOWED_HOSTS, ENV_DAEMON_PORT,
ENV_IDLE_GRACE_SECONDS, ENV_PID_FILE,
ENV_SNAPSHOT_ROOT, ENV_VERSION_ROOT)
from mirage.server.host_validation_constants import DEFAULT_ALLOWED_HOSTS
from mirage.server.paths import mirage_home
DEFAULT_DAEMON_URL = "http://127.0.0.1:8765"
_ENV_FOR_KEY = {
"url": ENV_DAEMON_URL,
"allowed_hosts": ENV_ALLOWED_HOSTS,
"auth_mode": ENV_AUTH_MODE,
"jwt_alg": ENV_JWT_ALG,
"jwt_issuer": ENV_JWT_ISSUER,
"jwt_audience": ENV_JWT_AUDIENCE,
"jwt_pubkey_file": ENV_JWT_PUBKEY_FILE,
"jwt_clock_skew": ENV_JWT_CLOCK_SKEW,
"jwt_authorized_parties": ENV_JWT_AUTHORIZED_PARTIES,
"auth_token": ENV_TOKEN,
"idle_grace_seconds": ENV_IDLE_GRACE_SECONDS,
"port": ENV_DAEMON_PORT,
"pid_file": ENV_PID_FILE,
"version_root": ENV_VERSION_ROOT,
"snapshot_root": ENV_SNAPSHOT_ROOT,
}
@dataclass
class DaemonSettings:
url: str = DEFAULT_DAEMON_URL
socket: str = ""
auth_token: str = ""
idle_grace_seconds: float = 30.0
def config_path() -> Path:
return mirage_home() / "config.toml"
def load_daemon_settings(path: Path | None = None) -> DaemonSettings:
"""Load daemon settings, applying the override chain.
Order of precedence (highest first):
1. ``MIRAGE_DAEMON_URL`` env var
2. ``MIRAGE_TOKEN`` env var
3. values in ``$MIRAGE_HOME/config.toml`` (default
``~/.mirage/config.toml``) ``[daemon]`` table
4. defaults
Args:
path (Path | None): config file location. Defaults to
``config_path()``.
Returns:
DaemonSettings: resolved settings.
"""
use_path = path or config_path()
if path is not None:
if use_path.exists():
with open(use_path, "rb") as f:
table = tomllib.load(f).get("daemon", {})
else:
table = {}
else:
table = read_daemon_table(mirage_home())
settings = DaemonSettings(
url=str(table.get("url", DEFAULT_DAEMON_URL)),
socket=str(table.get("socket", "")),
auth_token=str(table.get("auth_token", "")),
idle_grace_seconds=float(table.get("idle_grace_seconds", 30.0)),
)
env_url = os.environ.get(ENV_DAEMON_URL)
if env_url:
settings.url = env_url
env_token = os.environ.get(ENV_TOKEN)
if env_token:
settings.auth_token = env_token
if not settings.auth_token:
file_token = auth_storage.read_token_file(
auth_storage.default_token_file())
if file_token:
settings.auth_token = file_token
return settings
def _default_for_key(key: str, home: Path) -> str:
defaults = {
"url": DEFAULT_DAEMON_URL,
"allowed_hosts": ",".join(DEFAULT_ALLOWED_HOSTS),
"auth_mode": "local",
"jwt_alg": "",
"jwt_issuer": "",
"jwt_audience": "",
"jwt_pubkey_file": "",
"jwt_clock_skew": "5",
"jwt_authorized_parties": "",
"socket": "",
"auth_token": "",
"idle_grace_seconds": "30",
"port": "8765",
"pid_file": str(home / "daemon.pid"),
"version_root": str(home / "repos"),
"snapshot_root": str(home / "snapshots"),
}
return defaults[key]
def resolved_config() -> dict[str, tuple[str, str]]:
"""Resolve every config key to its effective value and origin.
Origin is ``"env <NAME>"``, ``"file"``, or ``"default"``, applying
the same precedence the daemon and CLI use (env > file > default;
explicit per-call arguments are not represented here).
Returns:
dict[str, tuple[str, str]]: key to (effective value, origin).
"""
home = mirage_home()
table = read_daemon_table(home)
out: dict[str, tuple[str, str]] = {}
for key in sorted(ALLOWED_KEYS):
env_name = _ENV_FOR_KEY.get(key)
env_value = os.environ.get(env_name) if env_name else None
if env_value:
out[key] = (env_value, f"env {env_name}")
elif str(table.get(key, "")):
out[key] = (str(table[key]), "file")
else:
out[key] = (_default_for_key(key, home), "default")
return out
def _check_key(key: str) -> None:
if key not in ALLOWED_KEYS:
raise DaemonConfigError(f"unknown config key: {key!r}; allowed: "
f"{', '.join(sorted(ALLOWED_KEYS))}")
def _format_value(key: str, value: str) -> str:
if key in NUMERIC_KEYS:
return value
escaped = value.replace("\\", "\\\\").replace('"', '\\"')
return f'"{escaped}"'
def _config_lines(path: Path) -> list[str]:
if not path.exists():
return []
return path.read_text().splitlines()
def list_config(path: Path | None = None) -> dict:
"""Return the ``[daemon]`` table as written in the config file.
Args:
path (Path | None): config file. Defaults to ``config_path()``.
Returns:
dict: file-level key/value strings (no env or default folding).
Raises:
DaemonConfigError: the file exists but is not valid TOML.
"""
use_path = path or config_path()
if not use_path.exists():
return {}
try:
with open(use_path, "rb") as f:
table = tomllib.load(f).get("daemon", {})
except tomllib.TOMLDecodeError as e:
raise DaemonConfigError(f"malformed {use_path}: {e}") from e
return {k: str(v) for k, v in table.items()}
def get_config(key: str, path: Path | None = None) -> str | None:
"""Return one ``[daemon]`` key's file value, or ``None`` if unset.
Args:
key (str): a key in :data:`ALLOWED_KEYS`.
path (Path | None): config file. Defaults to ``config_path()``.
Returns:
str | None: the value, or ``None`` if absent.
"""
_check_key(key)
return list_config(path).get(key)
def set_config(key: str, value: str, path: Path | None = None) -> None:
"""Write ``key = value`` into the ``[daemon]`` table, in place.
Creates the file and ``[daemon]`` header if missing, updates the key
line if present, otherwise appends it inside ``[daemon]``. Comments
and unrelated lines are preserved.
Args:
key (str): a key in :data:`ALLOWED_KEYS`.
value (str): the value to store.
path (Path | None): config file. Defaults to ``config_path()``.
"""
_check_key(key)
use_path = path or config_path()
lines = _config_lines(use_path)
rendered = f"{key} = {_format_value(key, value)}"
header_idx = None
for i, line in enumerate(lines):
if line.strip() == "[daemon]":
header_idx = i
break
if header_idx is None:
if lines and lines[-1].strip() != "":
lines.append("")
lines.append("[daemon]")
lines.append(rendered)
else:
end = len(lines)
for i in range(header_idx + 1, len(lines)):
if lines[i].strip().startswith("["):
end = i
break
for i in range(header_idx + 1, end):
stripped = lines[i].strip()
if stripped.startswith("#") or "=" not in stripped:
continue
if stripped.split("=", 1)[0].strip() == key:
lines[i] = rendered
break
else:
lines.insert(end, rendered)
use_path.parent.mkdir(parents=True, exist_ok=True)
use_path.write_text("\n".join(lines) + "\n")
os.chmod(use_path, 0o600)
def unset_config(key: str, path: Path | None = None) -> None:
"""Remove ``key`` from the ``[daemon]`` table if present.
Unknown keys are allowed so a file the daemon refuses to load can
be repaired from the CLI.
Args:
key (str): any key present in the file.
path (Path | None): config file. Defaults to ``config_path()``.
"""
use_path = path or config_path()
if not use_path.exists():
return
lines = _config_lines(use_path)
kept = []
in_daemon = False
for line in lines:
stripped = line.strip()
if stripped == "[daemon]":
in_daemon = True
kept.append(line)
continue
if stripped.startswith("["):
in_daemon = False
if (in_daemon and "=" in stripped and not stripped.startswith("#")
and stripped.split("=", 1)[0].strip() == key):
continue
kept.append(line)
use_path.write_text("\n".join(kept) + "\n")
os.chmod(use_path, 0o600)