chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
# ========= 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 subprocess
|
||||
import sys
|
||||
import time
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import httpx
|
||||
|
||||
from mirage.cli.env import ENV_AUTH_MODE, ENV_AUTH_TOKEN
|
||||
from mirage.cli.settings import DaemonSettings, load_daemon_settings
|
||||
from mirage.server.auth import AuthMode
|
||||
from mirage.server.auth import storage as auth_storage
|
||||
from mirage.server.daemon_config import (read_daemon_table,
|
||||
validate_daemon_table)
|
||||
from mirage.server.env import ENV_DAEMON_PORT
|
||||
from mirage.server.paths import mirage_home
|
||||
|
||||
|
||||
class DaemonUnreachable(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class DaemonClient:
|
||||
"""Thin httpx wrapper around the Mirage daemon REST API.
|
||||
|
||||
Constructs once per CLI invocation and is reused across calls.
|
||||
Handles auth header injection, daemon auto-spawn on first
|
||||
``workspace --create``, and discovery via the settings chain.
|
||||
"""
|
||||
|
||||
def __init__(self, settings: DaemonSettings) -> None:
|
||||
self.settings = settings
|
||||
self._client = httpx.Client(base_url=settings.url, timeout=60.0)
|
||||
|
||||
def __enter__(self) -> "DaemonClient":
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc_info) -> None:
|
||||
self._client.close()
|
||||
|
||||
def _headers(self) -> dict[str, str]:
|
||||
if self.settings.auth_token:
|
||||
return {"Authorization": f"Bearer {self.settings.auth_token}"}
|
||||
return {}
|
||||
|
||||
def request(self, method: str, path: str, **kwargs) -> httpx.Response:
|
||||
headers = {**self._headers(), **kwargs.pop("headers", {})}
|
||||
return self._client.request(method, path, headers=headers, **kwargs)
|
||||
|
||||
def is_reachable(self, timeout: float = 0.5) -> bool:
|
||||
try:
|
||||
r = self._client.get("/v1/health",
|
||||
timeout=timeout,
|
||||
headers=self._headers())
|
||||
return r.status_code == 200
|
||||
except httpx.RequestError:
|
||||
return False
|
||||
|
||||
def ensure_running(self,
|
||||
startup_timeout: float = 5.0,
|
||||
allow_spawn: bool = True) -> None:
|
||||
"""Ensure the daemon is reachable, optionally spawning it.
|
||||
|
||||
Args:
|
||||
startup_timeout (float): seconds to wait for the spawned
|
||||
daemon to answer ``/v1/health``.
|
||||
allow_spawn (bool): if True and the daemon is unreachable,
|
||||
fork-execs ``uvicorn mirage.server.app:app`` detached.
|
||||
|
||||
Raises:
|
||||
DaemonUnreachable: daemon is not reachable and either
|
||||
``allow_spawn=False`` or the spawned daemon failed to
|
||||
come up within ``startup_timeout``.
|
||||
"""
|
||||
if self.is_reachable():
|
||||
return
|
||||
if not allow_spawn:
|
||||
raise DaemonUnreachable(
|
||||
f"daemon not reachable at {self.settings.url}; "
|
||||
"run `mirage workspace --create CONFIG.yaml` to spawn one")
|
||||
self._spawn_daemon()
|
||||
deadline = time.monotonic() + startup_timeout
|
||||
while time.monotonic() < deadline:
|
||||
if self.is_reachable(timeout=0.3):
|
||||
return
|
||||
time.sleep(0.1)
|
||||
raise DaemonUnreachable(
|
||||
f"daemon spawned but did not answer /v1/health within "
|
||||
f"{startup_timeout:.1f}s")
|
||||
|
||||
def _spawn_daemon(self) -> None:
|
||||
table = read_daemon_table(mirage_home())
|
||||
validate_daemon_table(table)
|
||||
port = self._resolve_port(table)
|
||||
env = dict(os.environ)
|
||||
if not self.settings.auth_token:
|
||||
self.settings.auth_token = auth_storage.ensure_token_file(
|
||||
auth_storage.default_token_file())
|
||||
env[ENV_AUTH_TOKEN] = self.settings.auth_token
|
||||
if ENV_AUTH_MODE not in env and not table.get("auth_mode"):
|
||||
env[ENV_AUTH_MODE] = AuthMode.LOCAL.value
|
||||
cmd = [
|
||||
sys.executable,
|
||||
"-m",
|
||||
"uvicorn",
|
||||
"mirage.server.daemon:app",
|
||||
"--host",
|
||||
"127.0.0.1",
|
||||
"--port",
|
||||
str(port),
|
||||
"--log-level",
|
||||
"warning",
|
||||
]
|
||||
log_dir = mirage_home()
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
log_file = log_dir / "daemon.log"
|
||||
with open(log_file, "ab") as f:
|
||||
subprocess.Popen(
|
||||
cmd,
|
||||
env=env,
|
||||
stdout=f,
|
||||
stderr=subprocess.STDOUT,
|
||||
start_new_session=True,
|
||||
)
|
||||
|
||||
def _resolve_port(self, table: dict) -> int:
|
||||
"""Resolve the listen port for a spawned daemon.
|
||||
|
||||
Priority: ``$MIRAGE_DAEMON_PORT``, then the ``port`` key in
|
||||
``config.toml`` ``[daemon]``, then the port embedded in the
|
||||
client ``url`` setting, then 8765.
|
||||
|
||||
Args:
|
||||
table (dict): the already-read ``[daemon]`` table.
|
||||
|
||||
Returns:
|
||||
int: the port to bind.
|
||||
"""
|
||||
env_port = os.environ.get(ENV_DAEMON_PORT)
|
||||
if env_port:
|
||||
return int(env_port)
|
||||
config_port = table.get("port")
|
||||
if config_port:
|
||||
return int(config_port)
|
||||
return self._port_from_url()
|
||||
|
||||
def _port_from_url(self) -> int:
|
||||
parsed = urlparse(self.settings.url)
|
||||
return parsed.port or 8765
|
||||
|
||||
|
||||
def make_client() -> DaemonClient:
|
||||
return DaemonClient(load_daemon_settings())
|
||||
Reference in New Issue
Block a user