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
293 lines
9.7 KiB
Python
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)
|