fix: all agent path prefixed by HIVE_HOME
This commit is contained in:
@@ -657,8 +657,10 @@ def write_compaction_debug_log(
|
||||
level: str,
|
||||
inventory: list[dict[str, Any]] | None,
|
||||
) -> None:
|
||||
"""Write detailed compaction analysis to ~/.hive/compaction_log/."""
|
||||
log_dir = Path.home() / ".hive" / "compaction_log"
|
||||
"""Write detailed compaction analysis to $HIVE_HOME/compaction_log/."""
|
||||
from framework.config import HIVE_HOME
|
||||
|
||||
log_dir = HIVE_HOME / "compaction_log"
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
ts = datetime.now(UTC).strftime("%Y%m%dT%H%M%S_%f")
|
||||
|
||||
@@ -560,7 +560,9 @@ class CredentialTesterAgent:
|
||||
if self._selected_account is None:
|
||||
raise RuntimeError("No account selected. Call select_account() first.")
|
||||
|
||||
self._storage_path = Path.home() / ".hive" / "agents" / "credential_tester"
|
||||
from framework.config import HIVE_HOME
|
||||
|
||||
self._storage_path = HIVE_HOME / "agents" / "credential_tester"
|
||||
self._storage_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
self._tool_registry = ToolRegistry()
|
||||
|
||||
@@ -66,7 +66,9 @@ def _get_last_active(agent_path: Path) -> str | None:
|
||||
latest: str | None = None
|
||||
|
||||
# 1. Worker sessions
|
||||
sessions_dir = Path.home() / ".hive" / "agents" / agent_name / "sessions"
|
||||
from framework.config import HIVE_HOME
|
||||
|
||||
sessions_dir = HIVE_HOME / "agents" / agent_name / "sessions"
|
||||
if sessions_dir.exists():
|
||||
for session_dir in sessions_dir.iterdir():
|
||||
if not session_dir.is_dir() or not session_dir.name.startswith("session_"):
|
||||
@@ -115,7 +117,9 @@ def _get_last_active(agent_path: Path) -> str | None:
|
||||
|
||||
def _count_sessions(agent_name: str) -> int:
|
||||
"""Count session directories under ~/.hive/agents/{agent_name}/sessions/."""
|
||||
sessions_dir = Path.home() / ".hive" / "agents" / agent_name / "sessions"
|
||||
from framework.config import HIVE_HOME
|
||||
|
||||
sessions_dir = HIVE_HOME / "agents" / agent_name / "sessions"
|
||||
if not sessions_dir.exists():
|
||||
return 0
|
||||
return sum(1 for d in sessions_dir.iterdir() if d.is_dir() and d.name.startswith("session_"))
|
||||
@@ -123,7 +127,9 @@ def _count_sessions(agent_name: str) -> int:
|
||||
|
||||
def _count_runs(agent_name: str) -> int:
|
||||
"""Count unique run_ids across all sessions for an agent."""
|
||||
sessions_dir = Path.home() / ".hive" / "agents" / agent_name / "sessions"
|
||||
from framework.config import HIVE_HOME
|
||||
|
||||
sessions_dir = HIVE_HOME / "agents" / agent_name / "sessions"
|
||||
if not sessions_dir.exists():
|
||||
return 0
|
||||
run_ids: set[str] = set()
|
||||
|
||||
@@ -6,8 +6,10 @@ from pathlib import Path
|
||||
|
||||
|
||||
def _load_preferred_model() -> str:
|
||||
"""Load preferred model from ~/.hive/configuration.json."""
|
||||
config_path = Path.home() / ".hive" / "configuration.json"
|
||||
"""Load preferred model from $HIVE_HOME/configuration.json."""
|
||||
from framework.config import HIVE_HOME
|
||||
|
||||
config_path = HIVE_HOME / "configuration.json"
|
||||
if config_path.exists():
|
||||
try:
|
||||
with open(config_path, encoding="utf-8") as f:
|
||||
|
||||
@@ -18,7 +18,12 @@ from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CREDENTIAL_KEY_PATH = Path.home() / ".hive" / "secrets" / "credential_key"
|
||||
# Resolved once at module import. ``framework.config.HIVE_HOME`` reads
|
||||
# the desktop's ``HIVE_HOME`` env var at its own import time, so the
|
||||
# runtime always sees the per-user root before this constant is computed.
|
||||
from framework.config import HIVE_HOME as _HIVE_HOME
|
||||
|
||||
CREDENTIAL_KEY_PATH = _HIVE_HOME / "secrets" / "credential_key"
|
||||
CREDENTIAL_KEY_ENV_VAR = "HIVE_CREDENTIAL_KEY"
|
||||
ADEN_CREDENTIAL_ID = "aden_api_key"
|
||||
ADEN_ENV_VAR = "ADEN_API_KEY"
|
||||
|
||||
@@ -751,7 +751,9 @@ class CredentialStore:
|
||||
|
||||
# Determine local storage path
|
||||
if local_path is None:
|
||||
local_path = str(Path.home() / ".hive" / "credentials")
|
||||
from framework.config import HIVE_HOME
|
||||
|
||||
local_path = str(HIVE_HOME / "credentials")
|
||||
|
||||
local_storage = EncryptedFileStorage(base_path=local_path)
|
||||
|
||||
|
||||
@@ -42,7 +42,9 @@ def _open_event_log() -> IO[str] | None:
|
||||
return None
|
||||
raw = _DEBUG_EVENTS_RAW
|
||||
if raw.lower() in ("1", "true", "full"):
|
||||
log_dir = Path.home() / ".hive" / "event_logs"
|
||||
from framework.config import HIVE_HOME
|
||||
|
||||
log_dir = HIVE_HOME / "event_logs"
|
||||
else:
|
||||
log_dir = Path(raw)
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -264,7 +264,9 @@ def ensure_all_colony_dbs(colonies_root: Path | None = None) -> list[Path]:
|
||||
run the stale-claim reclaimer on all of them in one pass.
|
||||
"""
|
||||
if colonies_root is None:
|
||||
colonies_root = Path.home() / ".hive" / "colonies"
|
||||
from framework.config import COLONIES_DIR
|
||||
|
||||
colonies_root = COLONIES_DIR
|
||||
if not colonies_root.is_dir():
|
||||
return []
|
||||
|
||||
|
||||
@@ -50,8 +50,10 @@ _ENDPOINTS = [
|
||||
_DEFAULT_PROJECT_ID = "rising-fact-p41fc"
|
||||
_TOKEN_REFRESH_BUFFER_SECS = 60
|
||||
|
||||
# Credentials file in ~/.hive/ (native implementation)
|
||||
_ACCOUNTS_FILE = Path.home() / ".hive" / "antigravity-accounts.json"
|
||||
# Credentials file in $HIVE_HOME (native implementation)
|
||||
from framework.config import HIVE_HOME as _HIVE_HOME
|
||||
|
||||
_ACCOUNTS_FILE = _HIVE_HOME / "antigravity-accounts.json"
|
||||
_IDE_STATE_DB_MAC = (
|
||||
Path.home() / "Library" / "Application Support" / "Antigravity" / "User" / "globalStorage" / "state.vscdb"
|
||||
)
|
||||
|
||||
@@ -45,17 +45,19 @@ logging.getLogger("httpcore").setLevel(logging.WARNING)
|
||||
|
||||
|
||||
def _api_base_needs_bearer_auth(api_base: str | None) -> bool:
|
||||
"""Return True when ``api_base`` points at an Anthropic-compatible endpoint
|
||||
"""Return True when api_base points at an Anthropic-compatible endpoint
|
||||
that authenticates via ``Authorization: Bearer`` rather than ``x-api-key``.
|
||||
|
||||
The hive-llm proxy (Rust service in hive-backend/llm/) speaks the Anthropic
|
||||
Messages API but mints user-scoped JWTs and validates them via Bearer auth.
|
||||
Default upstream Anthropic endpoints (api.anthropic.com, Kimi's
|
||||
api.kimi.com/coding) keep using x-api-key, so the override is scoped to the
|
||||
known hive-proxy hosts plus the env-configured override.
|
||||
The Hive LLM proxy (Rust service in hive-backend/llm/) speaks the
|
||||
Anthropic Messages API but mints user-scoped JWTs and validates them
|
||||
via Bearer auth. Default upstream Anthropic endpoints (api.anthropic.com,
|
||||
Kimi's api.kimi.com/coding) keep using x-api-key, so the override is
|
||||
scoped to known hive-proxy hosts plus the env-configured override.
|
||||
"""
|
||||
if not api_base:
|
||||
return False
|
||||
# Strip protocol, port, and path so a plain hostname compare is enough
|
||||
# for the common cases.
|
||||
lowered = api_base.lower()
|
||||
for host in ("adenhq.com", "open-hive.com", "127.0.0.1:8890", "localhost:8890"):
|
||||
if host in lowered:
|
||||
@@ -67,19 +69,16 @@ def _api_base_needs_bearer_auth(api_base: str | None) -> bool:
|
||||
|
||||
|
||||
def _patch_litellm_anthropic_oauth() -> None:
|
||||
"""Patch litellm's Anthropic header construction to fix OAuth/JWT token handling.
|
||||
"""Patch litellm's Anthropic header construction to fix OAuth token handling.
|
||||
|
||||
Two cases are remapped:
|
||||
1. **Anthropic OAuth tokens** (``sk-ant-oat`` prefix). litellm puts the token
|
||||
into ``x-api-key`` but Anthropic's API requires it on
|
||||
``Authorization: Bearer`` only — see BerriAI/litellm#19618.
|
||||
2. **Hive LLM proxy bearer tokens** (any JWT). The Rust proxy at
|
||||
hive-backend/llm/ speaks the Anthropic Messages API but authenticates
|
||||
with ``Authorization: Bearer <jwt>``; litellm's default ``x-api-key``
|
||||
would 401.
|
||||
litellm bug: validate_environment() puts the OAuth token into x-api-key,
|
||||
but Anthropic's API rejects OAuth tokens in x-api-key. They must be sent
|
||||
via Authorization: Bearer only, with x-api-key omitted entirely.
|
||||
|
||||
Both cases share the same fix: promote whatever's in ``x-api-key`` to
|
||||
``Authorization: Bearer`` and drop ``x-api-key``.
|
||||
This patch wraps validate_environment to remove x-api-key when the
|
||||
Authorization header carries an OAuth token (sk-ant-oat prefix).
|
||||
|
||||
See: https://github.com/BerriAI/litellm/issues/19618
|
||||
"""
|
||||
try:
|
||||
from litellm.llms.anthropic.common_utils import AnthropicModelInfo
|
||||
@@ -120,15 +119,6 @@ def _patch_litellm_anthropic_oauth() -> None:
|
||||
oauth_prefix = f"Bearer {ANTHROPIC_OAUTH_TOKEN_PREFIX}"
|
||||
auth_is_oauth = auth.startswith(oauth_prefix)
|
||||
key_is_oauth = x_api_key.startswith(ANTHROPIC_OAUTH_TOKEN_PREFIX)
|
||||
# The hive-llm proxy speaks the Anthropic Messages API but authenticates
|
||||
# via Authorization: Bearer <jwt>; x-api-key is ignored and returns
|
||||
# missing_auth. Promote x-api-key → Authorization for those endpoints
|
||||
# so hive's per-user stream JWT actually authenticates the request.
|
||||
hive_needs_remap = (
|
||||
x_api_key
|
||||
and not auth
|
||||
and _api_base_needs_bearer_auth(api_base)
|
||||
)
|
||||
if auth_is_oauth or key_is_oauth:
|
||||
token = x_api_key if key_is_oauth else auth.removeprefix("Bearer ").strip()
|
||||
result.pop("x-api-key", None)
|
||||
@@ -139,9 +129,6 @@ def _patch_litellm_anthropic_oauth() -> None:
|
||||
if ANTHROPIC_OAUTH_BETA_HEADER not in beta_parts:
|
||||
beta_parts.append(ANTHROPIC_OAUTH_BETA_HEADER)
|
||||
result["anthropic-beta"] = ",".join(beta_parts)
|
||||
elif hive_needs_remap:
|
||||
result.pop("x-api-key", None)
|
||||
result["authorization"] = f"Bearer {x_api_key}"
|
||||
return result
|
||||
|
||||
AnthropicModelInfo.validate_environment = _patched_validate_environment
|
||||
@@ -390,10 +377,16 @@ OPENROUTER_TOOL_COMPAT_MODEL_CACHE: dict[str, float] = {}
|
||||
# from rate-limit retries — 3 retries is sufficient for connection failures.
|
||||
STREAM_TRANSIENT_MAX_RETRIES = 3
|
||||
|
||||
# Directory for dumping failed requests
|
||||
FAILED_REQUESTS_DIR = Path.home() / ".hive" / "failed_requests"
|
||||
# Directory for dumping failed requests. Resolved lazily so HIVE_HOME
|
||||
# overrides (set by the desktop shell) take effect even if this module
|
||||
# is imported before framework.config picks up the override.
|
||||
def _failed_requests_dir() -> Path:
|
||||
from framework.config import HIVE_HOME
|
||||
|
||||
# Maximum number of dump files to retain in ~/.hive/failed_requests/.
|
||||
return HIVE_HOME / "failed_requests"
|
||||
|
||||
|
||||
# Maximum number of dump files to retain in $HIVE_HOME/failed_requests/.
|
||||
# Older files are pruned automatically to prevent unbounded disk growth.
|
||||
MAX_FAILED_REQUEST_DUMPS = 50
|
||||
|
||||
@@ -585,7 +578,7 @@ def _prune_failed_request_dumps(max_files: int = MAX_FAILED_REQUEST_DUMPS) -> No
|
||||
"""
|
||||
try:
|
||||
all_dumps = sorted(
|
||||
FAILED_REQUESTS_DIR.glob("*.json"),
|
||||
_failed_requests_dir().glob("*.json"),
|
||||
key=lambda f: f.stat().st_mtime,
|
||||
)
|
||||
excess = len(all_dumps) - max_files
|
||||
@@ -620,11 +613,12 @@ def _dump_failed_request(
|
||||
) -> str:
|
||||
"""Dump failed request to a file for debugging. Returns the file path."""
|
||||
try:
|
||||
FAILED_REQUESTS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
dump_dir = _failed_requests_dir()
|
||||
dump_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
||||
filename = f"{error_type}_{model.replace('/', '_')}_{timestamp}.json"
|
||||
filepath = FAILED_REQUESTS_DIR / filename
|
||||
filepath = dump_dir / filename
|
||||
|
||||
# Build dump data
|
||||
messages = kwargs.get("messages", [])
|
||||
@@ -654,7 +648,7 @@ def _dump_failed_request(
|
||||
|
||||
return str(filepath)
|
||||
except OSError as e:
|
||||
logger.warning(f"Failed to dump request debug log to {FAILED_REQUESTS_DIR}: {e}")
|
||||
logger.warning(f"Failed to dump request debug log to {_failed_requests_dir()}: {e}")
|
||||
return "log_write_failed"
|
||||
|
||||
|
||||
@@ -2207,9 +2201,10 @@ class LiteLLMProvider(LLMProvider):
|
||||
if logger.isEnabledFor(logging.DEBUG) and full_messages:
|
||||
import json as _json
|
||||
from datetime import datetime as _dt
|
||||
from pathlib import Path as _Path
|
||||
|
||||
_debug_dir = _Path.home() / ".hive" / "debug_logs"
|
||||
from framework.config import HIVE_HOME as _HIVE_HOME
|
||||
|
||||
_debug_dir = _HIVE_HOME / "debug_logs"
|
||||
_debug_dir.mkdir(parents=True, exist_ok=True)
|
||||
_ts = _dt.now().strftime("%Y%m%d_%H%M%S_%f")
|
||||
_dump_file = _debug_dir / f"llm_request_{_ts}.json"
|
||||
|
||||
@@ -558,7 +558,9 @@ ANTIGRAVITY_IDE_STATE_DB = (
|
||||
# Linux fallback for the IDE state DB
|
||||
ANTIGRAVITY_IDE_STATE_DB_LINUX = Path.home() / ".config" / "Antigravity" / "User" / "globalStorage" / "state.vscdb"
|
||||
# Antigravity credentials stored by native OAuth implementation
|
||||
ANTIGRAVITY_AUTH_FILE = Path.home() / ".hive" / "antigravity-accounts.json"
|
||||
from framework.config import HIVE_HOME as _HIVE_HOME
|
||||
|
||||
ANTIGRAVITY_AUTH_FILE = _HIVE_HOME / "antigravity-accounts.json"
|
||||
|
||||
ANTIGRAVITY_OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token"
|
||||
_ANTIGRAVITY_TOKEN_LIFETIME_SECS = 3600 # Google access tokens expire in 1 hour
|
||||
@@ -1389,7 +1391,7 @@ class AgentLoader:
|
||||
)
|
||||
|
||||
if storage_path is None:
|
||||
storage_path = Path.home() / ".hive" / "agents" / agent_path.name / worker_name
|
||||
storage_path = _HIVE_HOME / "agents" / agent_path.name / worker_name
|
||||
storage_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
runner = cls(
|
||||
|
||||
@@ -62,10 +62,14 @@ _STALE_DEFAULT_ALIASES: dict[str, str] = {
|
||||
|
||||
|
||||
class MCPRegistry:
|
||||
"""Manages local MCP server state in ~/.hive/mcp_registry/."""
|
||||
"""Manages local MCP server state in $HIVE_HOME/mcp_registry/."""
|
||||
|
||||
def __init__(self, base_path: Path | None = None):
|
||||
self._base = base_path or Path.home() / ".hive" / "mcp_registry"
|
||||
if base_path is None:
|
||||
from framework.config import HIVE_HOME
|
||||
|
||||
base_path = HIVE_HOME / "mcp_registry"
|
||||
self._base = base_path
|
||||
self._installed_path = self._base / "installed.json"
|
||||
self._config_path = self._base / "config.json"
|
||||
self._cache_dir = self._base / "cache"
|
||||
|
||||
@@ -515,7 +515,9 @@ class ToolRegistry:
|
||||
if "--project-root" not in args:
|
||||
args.extend(["--project-root", project_root])
|
||||
if "--write-root" not in args:
|
||||
_write_root = Path.home() / ".hive" / "workspace"
|
||||
from framework.config import HIVE_HOME
|
||||
|
||||
_write_root = HIVE_HOME / "workspace"
|
||||
_write_root.mkdir(parents=True, exist_ok=True)
|
||||
args.extend(["--write-root", str(_write_root)])
|
||||
config["args"] = args
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""aiohttp Application factory for the Hive HTTP API server."""
|
||||
|
||||
import hmac
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
@@ -21,7 +22,9 @@ _ALLOWED_AGENT_ROOTS: tuple[Path, ...] | None = None
|
||||
|
||||
def _has_encrypted_credentials() -> bool:
|
||||
"""Return True when an encrypted credential store already exists on disk."""
|
||||
cred_dir = Path.home() / ".hive" / "credentials" / "credentials"
|
||||
from framework.config import HIVE_HOME
|
||||
|
||||
cred_dir = HIVE_HOME / "credentials" / "credentials"
|
||||
return cred_dir.is_dir() and any(cred_dir.glob("*.enc"))
|
||||
|
||||
|
||||
@@ -30,17 +33,18 @@ def _get_allowed_agent_roots() -> tuple[Path, ...]:
|
||||
|
||||
Roots are anchored to the repository root (derived from ``__file__``)
|
||||
so the allowlist is correct regardless of the process's working
|
||||
directory.
|
||||
directory. The hive-home subtrees honour ``HIVE_HOME`` so the desktop's
|
||||
per-user root is allowed in addition to (or instead of) ``~/.hive``.
|
||||
"""
|
||||
global _ALLOWED_AGENT_ROOTS
|
||||
if _ALLOWED_AGENT_ROOTS is None:
|
||||
from framework.config import COLONIES_DIR
|
||||
from framework.config import COLONIES_DIR, HIVE_HOME
|
||||
|
||||
_ALLOWED_AGENT_ROOTS = (
|
||||
COLONIES_DIR.resolve(), # ~/.hive/colonies/
|
||||
COLONIES_DIR.resolve(), # $HIVE_HOME/colonies/
|
||||
(_REPO_ROOT / "exports").resolve(), # compat fallback
|
||||
(_REPO_ROOT / "examples").resolve(),
|
||||
(Path.home() / ".hive" / "agents").resolve(),
|
||||
(HIVE_HOME / "agents").resolve(),
|
||||
)
|
||||
return _ALLOWED_AGENT_ROOTS
|
||||
|
||||
@@ -62,7 +66,8 @@ def validate_agent_path(agent_path: str | Path) -> Path:
|
||||
if resolved.is_relative_to(root) and resolved != root:
|
||||
return resolved
|
||||
raise ValueError(
|
||||
"agent_path must be inside an allowed directory (~/.hive/colonies/, exports/, examples/, or ~/.hive/agents/)"
|
||||
"agent_path must be inside an allowed directory "
|
||||
"($HIVE_HOME/colonies/, exports/, examples/, or $HIVE_HOME/agents/)"
|
||||
)
|
||||
|
||||
|
||||
@@ -94,13 +99,15 @@ def resolve_session(request: web.Request):
|
||||
def sessions_dir(session: Session) -> Path:
|
||||
"""Resolve the worker sessions directory for a session.
|
||||
|
||||
Storage layout: ~/.hive/agents/{agent_name}/sessions/
|
||||
Storage layout: $HIVE_HOME/agents/{agent_name}/sessions/
|
||||
Requires a worker to be loaded (worker_path must be set).
|
||||
"""
|
||||
if session.worker_path is None:
|
||||
raise ValueError("No worker loaded — no worker sessions directory")
|
||||
from framework.config import HIVE_HOME
|
||||
|
||||
agent_name = session.worker_path.name
|
||||
return Path.home() / ".hive" / "agents" / agent_name / "sessions"
|
||||
return HIVE_HOME / "agents" / agent_name / "sessions"
|
||||
|
||||
|
||||
# Allowed CORS origins (localhost on any port)
|
||||
@@ -159,6 +166,28 @@ async def no_cache_api_middleware(request: web.Request, handler):
|
||||
return response
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Desktop shared-secret auth middleware.
|
||||
#
|
||||
# When the runtime is spawned by the Electron main process, a fresh random
|
||||
# token is passed via ``HIVE_DESKTOP_TOKEN``. Every request from main must
|
||||
# carry the matching ``X-Hive-Token`` header. If the env var is unset (e.g.
|
||||
# running ``hive serve`` directly from a terminal), the check is skipped —
|
||||
# OSS behaviour is preserved.
|
||||
# ---------------------------------------------------------------------------
|
||||
_EXPECTED_DESKTOP_TOKEN: str | None = os.environ.get("HIVE_DESKTOP_TOKEN") or None
|
||||
|
||||
|
||||
@web.middleware
|
||||
async def desktop_auth_middleware(request: web.Request, handler):
|
||||
if _EXPECTED_DESKTOP_TOKEN is None:
|
||||
return await handler(request)
|
||||
provided = request.headers.get("X-Hive-Token", "")
|
||||
if not hmac.compare_digest(provided, _EXPECTED_DESKTOP_TOKEN):
|
||||
return web.json_response({"error": "unauthorized"}, status=401)
|
||||
return await handler(request)
|
||||
|
||||
|
||||
@web.middleware
|
||||
async def error_middleware(request: web.Request, handler):
|
||||
"""Catch exceptions and return JSON error responses.
|
||||
@@ -287,7 +316,12 @@ def create_app(model: str | None = None) -> web.Application:
|
||||
Returns:
|
||||
Configured aiohttp Application ready to run.
|
||||
"""
|
||||
app = web.Application(middlewares=[cors_middleware, no_cache_api_middleware, error_middleware])
|
||||
# Desktop mode: the runtime is always a subprocess of the Electron main
|
||||
# process, which reaches it via IPC and the `hive://` custom protocol.
|
||||
# There is no browser origin to authorize, so CORS is unnecessary.
|
||||
# The auth middleware enforces the shared-secret token when the env var
|
||||
# is set (i.e. when Electron spawned us); it is a no-op otherwise.
|
||||
app = web.Application(middlewares=[desktop_auth_middleware, no_cache_api_middleware, error_middleware])
|
||||
|
||||
# Initialize credential store (before SessionManager so it can be shared)
|
||||
from framework.credentials.store import CredentialStore
|
||||
@@ -392,9 +426,22 @@ def create_app(model: str | None = None) -> web.Application:
|
||||
register_skills_routes(app)
|
||||
register_task_routes(app)
|
||||
|
||||
# Static file serving — Option C production mode
|
||||
# If frontend/dist/ exists, serve built frontend files on /
|
||||
_setup_static_serving(app)
|
||||
# Commercial extensions (optional — only present in hive-desktop-runtime).
|
||||
# Imports lazily so an OSS install without the `commercial` package keeps
|
||||
# working unchanged.
|
||||
try:
|
||||
from commercial.middleware import setup_commercial_middleware
|
||||
from commercial.routes import register_routes as register_commercial_routes
|
||||
|
||||
setup_commercial_middleware(app)
|
||||
register_commercial_routes(app)
|
||||
logger.info("Commercial extensions loaded")
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Desktop mode: no static file serving. The frontend lives in the
|
||||
# Electron renderer process and is loaded from file:// (or the Vite
|
||||
# dev server in dev mode) — not from this aiohttp app.
|
||||
|
||||
return app
|
||||
|
||||
|
||||
@@ -294,7 +294,9 @@ def _resolve_progress_db_by_name(colony_name: str) -> Path | None:
|
||||
"""
|
||||
if not _COLONY_NAME_RE.match(colony_name):
|
||||
return None
|
||||
db_path = Path.home() / ".hive" / "colonies" / colony_name / "data" / "progress.db"
|
||||
from framework.config import COLONIES_DIR
|
||||
|
||||
db_path = COLONIES_DIR / colony_name / "data" / "progress.db"
|
||||
return db_path if db_path.exists() else None
|
||||
|
||||
|
||||
|
||||
@@ -1245,7 +1245,9 @@ async def fork_session_into_colony(
|
||||
# would wrongly flag every fresh colony as "already-exists" if we
|
||||
# used ``not colony_dir.exists()``. A colony is "new" until its
|
||||
# worker config has actually been written.
|
||||
colony_dir = Path.home() / ".hive" / "colonies" / colony_name
|
||||
from framework.config import COLONIES_DIR
|
||||
|
||||
colony_dir = COLONIES_DIR / colony_name
|
||||
worker_name = "worker"
|
||||
worker_config_path = colony_dir / f"{worker_name}.json"
|
||||
is_new = not worker_config_path.exists()
|
||||
@@ -1469,7 +1471,9 @@ async def fork_session_into_colony(
|
||||
|
||||
compaction_status.mark_in_progress(dest_queen_dir)
|
||||
|
||||
_worker_storage = Path.home() / ".hive" / "agents" / colony_name / worker_name
|
||||
from framework.config import HIVE_HOME
|
||||
|
||||
_worker_storage = HIVE_HOME / "agents" / colony_name / worker_name
|
||||
_dest_queen_dir = dest_queen_dir
|
||||
_queen_ctx = queen_ctx
|
||||
_queen_loop = queen_loop
|
||||
|
||||
@@ -1094,8 +1094,10 @@ async def handle_delete_agent(request: web.Request) -> web.Response:
|
||||
except ValueError as exc:
|
||||
return web.json_response({"error": str(exc)}, status=400)
|
||||
|
||||
# Reject deletion of framework agents (~/.hive/agents/) — those are internal
|
||||
hive_agents_dir = Path.home() / ".hive" / "agents"
|
||||
# Reject deletion of framework agents ($HIVE_HOME/agents/) — those are internal
|
||||
from framework.config import HIVE_HOME
|
||||
|
||||
hive_agents_dir = HIVE_HOME / "agents"
|
||||
if resolved.is_relative_to(hive_agents_dir):
|
||||
return web.json_response({"error": "Cannot delete framework agents"}, status=403)
|
||||
|
||||
|
||||
@@ -67,10 +67,10 @@ async def handle_list_nodes(request: web.Request) -> web.Response:
|
||||
worker_session_id = request.query.get("session_id")
|
||||
if worker_session_id and session.worker_path:
|
||||
worker_session_id = safe_path_segment(worker_session_id)
|
||||
from pathlib import Path
|
||||
from framework.config import HIVE_HOME
|
||||
|
||||
state_path = (
|
||||
Path.home() / ".hive" / "agents" / session.worker_path.name / "sessions" / worker_session_id / "state.json"
|
||||
HIVE_HOME / "agents" / session.worker_path.name / "sessions" / worker_session_id / "state.json"
|
||||
)
|
||||
if state_path.exists():
|
||||
try:
|
||||
|
||||
@@ -546,8 +546,10 @@ class SessionManager:
|
||||
session.colony_name = colony_id
|
||||
session.worker_path = agent_path
|
||||
|
||||
# Worker storage: ~/.hive/agents/{colony_name}/{worker_name}/
|
||||
worker_storage = Path.home() / ".hive" / "agents" / colony_id / worker_name
|
||||
# Worker storage: $HIVE_HOME/agents/{colony_name}/{worker_name}/
|
||||
from framework.config import HIVE_HOME
|
||||
|
||||
worker_storage = HIVE_HOME / "agents" / colony_id / worker_name
|
||||
worker_storage.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Copy conversations from colony if fresh
|
||||
@@ -927,7 +929,9 @@ class SessionManager:
|
||||
that process is still running on the host. If it is, the session is
|
||||
owned by another healthy worker process, so leave it alone.
|
||||
"""
|
||||
sessions_path = Path.home() / ".hive" / "agents" / agent_path.name / "sessions"
|
||||
from framework.config import HIVE_HOME
|
||||
|
||||
sessions_path = HIVE_HOME / "agents" / agent_path.name / "sessions"
|
||||
if not sessions_path.exists():
|
||||
return
|
||||
|
||||
|
||||
@@ -18,11 +18,22 @@ from pathlib import Path
|
||||
from framework.skills.parser import ParsedSkill
|
||||
from framework.skills.skill_errors import SkillError, SkillErrorCode
|
||||
|
||||
# Default install destination for user-scope skills
|
||||
USER_SKILLS_DIR = Path.home() / ".hive" / "skills"
|
||||
# Default install destination for user-scope skills + sentinel file for
|
||||
# the one-time security notice on first install (NFR-5). Computed via
|
||||
# helpers so HIVE_HOME (set by the desktop shell to a per-user dir)
|
||||
# is honoured. ``framework.config.HIVE_HOME`` is module-global and
|
||||
# resolved at first import — so a single call here is enough; we don't
|
||||
# need to re-resolve on every access.
|
||||
def _user_skills_dir() -> Path:
|
||||
from framework.config import HIVE_HOME
|
||||
|
||||
# Sentinel file for the one-time security notice on first install (NFR-5)
|
||||
INSTALL_NOTICE_SENTINEL = Path.home() / ".hive" / ".install_notice_shown"
|
||||
return HIVE_HOME / "skills"
|
||||
|
||||
|
||||
def _install_notice_sentinel() -> Path:
|
||||
from framework.config import HIVE_HOME
|
||||
|
||||
return HIVE_HOME / ".install_notice_shown"
|
||||
|
||||
_INSTALL_NOTICE = """\
|
||||
─────────────────────────────────────────────────────────────
|
||||
@@ -44,15 +55,16 @@ _INSTALL_NOTICE = """\
|
||||
def maybe_show_install_notice() -> None:
|
||||
"""Print a one-time security notice before the first skill install (NFR-5).
|
||||
|
||||
Touches a sentinel file in ~/.hive/ after showing the notice so it is
|
||||
Touches a sentinel file in $HIVE_HOME after showing the notice so it is
|
||||
only displayed once across all future installs.
|
||||
"""
|
||||
if INSTALL_NOTICE_SENTINEL.exists():
|
||||
sentinel = _install_notice_sentinel()
|
||||
if sentinel.exists():
|
||||
return
|
||||
print(_INSTALL_NOTICE, flush=True)
|
||||
try:
|
||||
INSTALL_NOTICE_SENTINEL.parent.mkdir(parents=True, exist_ok=True)
|
||||
INSTALL_NOTICE_SENTINEL.touch()
|
||||
sentinel.parent.mkdir(parents=True, exist_ok=True)
|
||||
sentinel.touch()
|
||||
except OSError:
|
||||
pass # If we can't write the sentinel, just show the notice every time
|
||||
|
||||
@@ -93,7 +105,7 @@ def install_from_git(
|
||||
fix="Install git (https://git-scm.com/) and retry.",
|
||||
)
|
||||
|
||||
dest = (target_dir or USER_SKILLS_DIR) / skill_name
|
||||
dest = (target_dir or _user_skills_dir()) / skill_name
|
||||
if dest.exists():
|
||||
raise SkillError(
|
||||
code=SkillErrorCode.SKILL_ACTIVATION_FAILED,
|
||||
@@ -194,7 +206,7 @@ def remove_skill(name: str, skills_dir: Path | None = None) -> bool:
|
||||
Raises:
|
||||
SkillError: If the directory exists but cannot be removed.
|
||||
"""
|
||||
target = (skills_dir or USER_SKILLS_DIR) / name
|
||||
target = (skills_dir or _user_skills_dir()) / name
|
||||
if not target.exists():
|
||||
return False
|
||||
try:
|
||||
|
||||
@@ -26,9 +26,18 @@ _DEFAULT_REGISTRY_URL = (
|
||||
"https://raw.githubusercontent.com/hive-skill-registry/hive-skill-registry/main/skill_index.json"
|
||||
)
|
||||
|
||||
_CACHE_DIR = Path.home() / ".hive" / "registry_cache"
|
||||
_CACHE_INDEX_PATH = _CACHE_DIR / "skill_index.json"
|
||||
_CACHE_METADATA_PATH = _CACHE_DIR / "metadata.json"
|
||||
def _cache_dir() -> Path:
|
||||
from framework.config import HIVE_HOME
|
||||
|
||||
return HIVE_HOME / "registry_cache"
|
||||
|
||||
|
||||
def _cache_index_path() -> Path:
|
||||
return _cache_dir() / "skill_index.json"
|
||||
|
||||
|
||||
def _cache_metadata_path() -> Path:
|
||||
return _cache_dir() / "metadata.json"
|
||||
_CACHE_TTL_SECONDS = 3600 # 1 hour
|
||||
|
||||
|
||||
@@ -46,7 +55,7 @@ class RegistryClient:
|
||||
cache_dir: Path | None = None,
|
||||
) -> None:
|
||||
self._url = registry_url or os.environ.get("HIVE_REGISTRY_URL", _DEFAULT_REGISTRY_URL)
|
||||
cache_root = cache_dir or _CACHE_DIR
|
||||
cache_root = cache_dir or _cache_dir()
|
||||
self._index_path = cache_root / "skill_index.json"
|
||||
self._metadata_path = cache_root / "metadata.json"
|
||||
|
||||
|
||||
@@ -30,8 +30,16 @@ _ENV_TRUST_ALL = "HIVE_TRUST_PROJECT_SKILLS"
|
||||
# Env var for comma-separated own-remote glob patterns (e.g. "github.com/myorg/*").
|
||||
_ENV_OWN_REMOTES = "HIVE_OWN_REMOTES"
|
||||
|
||||
_TRUSTED_REPOS_PATH = Path.home() / ".hive" / "trusted_repos.json"
|
||||
_NOTICE_SENTINEL_PATH = Path.home() / ".hive" / ".skill_trust_notice_shown"
|
||||
def _trusted_repos_path() -> Path:
|
||||
from framework.config import HIVE_HOME
|
||||
|
||||
return HIVE_HOME / "trusted_repos.json"
|
||||
|
||||
|
||||
def _notice_sentinel_path() -> Path:
|
||||
from framework.config import HIVE_HOME
|
||||
|
||||
return HIVE_HOME / ".skill_trust_notice_shown"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -50,7 +58,7 @@ class TrustedRepoStore:
|
||||
"""Persists permanently-trusted repo keys to ~/.hive/trusted_repos.json."""
|
||||
|
||||
def __init__(self, path: Path | None = None) -> None:
|
||||
self._path = path or _TRUSTED_REPOS_PATH
|
||||
self._path = path or _trusted_repos_path()
|
||||
self._entries: dict[str, TrustedRepoEntry] = {}
|
||||
self._loaded = False
|
||||
|
||||
@@ -224,7 +232,9 @@ class ProjectTrustDetector:
|
||||
patterns.extend(p.strip() for p in raw.split(",") if p.strip())
|
||||
|
||||
# From ~/.hive/own_remotes file
|
||||
own_remotes_file = Path.home() / ".hive" / "own_remotes"
|
||||
from framework.config import HIVE_HOME
|
||||
|
||||
own_remotes_file = HIVE_HOME / "own_remotes"
|
||||
if own_remotes_file.is_file():
|
||||
try:
|
||||
for line in own_remotes_file.read_text(encoding="utf-8").splitlines():
|
||||
@@ -415,7 +425,8 @@ class TrustGate:
|
||||
|
||||
def _maybe_show_security_notice(self, Colors) -> None: # noqa: N803
|
||||
"""Show the one-time security notice if not already shown (NFR-5)."""
|
||||
if _NOTICE_SENTINEL_PATH.exists():
|
||||
sentinel = _notice_sentinel_path()
|
||||
if sentinel.exists():
|
||||
return
|
||||
self._print("")
|
||||
self._print(
|
||||
@@ -427,8 +438,8 @@ class TrustGate:
|
||||
)
|
||||
self._print("")
|
||||
try:
|
||||
_NOTICE_SENTINEL_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
_NOTICE_SENTINEL_PATH.touch()
|
||||
sentinel.parent.mkdir(parents=True, exist_ok=True)
|
||||
sentinel.touch()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
@@ -1699,7 +1699,9 @@ def register_queen_lifecycle_tools(
|
||||
# INSIDE it (project scope, colony-local). fork_session_into_colony
|
||||
# keys "is_new" off worker.json rather than the dir itself, so
|
||||
# pre-creating here does not wrongly flag fresh colonies as "old".
|
||||
colony_dir = Path.home() / ".hive" / "colonies" / cn
|
||||
from framework.config import COLONIES_DIR
|
||||
|
||||
colony_dir = COLONIES_DIR / cn
|
||||
try:
|
||||
colony_dir.mkdir(parents=True, exist_ok=True)
|
||||
except OSError as e:
|
||||
@@ -2479,7 +2481,9 @@ def register_queen_lifecycle_tools(
|
||||
ensure_progress_db as _ensure_db,
|
||||
)
|
||||
|
||||
colony_dir = _Path.home() / ".hive" / "colonies" / cn
|
||||
from framework.config import COLONIES_DIR as _COLONIES_DIR
|
||||
|
||||
colony_dir = _COLONIES_DIR / cn
|
||||
if not colony_dir.is_dir():
|
||||
return json.dumps({"error": f"colony '{cn}' not found"})
|
||||
|
||||
|
||||
@@ -15,7 +15,13 @@ from typing import IO, Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_LLM_DEBUG_DIR = Path.home() / ".hive" / "llm_logs"
|
||||
def _llm_debug_dir() -> Path:
|
||||
"""Resolve $HIVE_HOME/llm_logs lazily so the env override (set by the
|
||||
desktop) takes effect. A module-level constant would freeze whatever
|
||||
HIVE_HOME was at import time and miss late-bound test overrides."""
|
||||
from framework.config import HIVE_HOME
|
||||
|
||||
return HIVE_HOME / "llm_logs"
|
||||
|
||||
_log_file: IO[str] | None = None
|
||||
_log_ready = False # lazy init guard
|
||||
@@ -23,9 +29,10 @@ _log_ready = False # lazy init guard
|
||||
|
||||
def _open_log() -> IO[str] | None:
|
||||
"""Open the JSONL log file for this process."""
|
||||
_LLM_DEBUG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
debug_dir = _llm_debug_dir()
|
||||
debug_dir.mkdir(parents=True, exist_ok=True)
|
||||
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
path = _LLM_DEBUG_DIR / f"{ts}.jsonl"
|
||||
path = debug_dir / f"{ts}.jsonl"
|
||||
logger.info("LLM debug log → %s", path)
|
||||
return open(path, "a", encoding="utf-8") # noqa: SIM115
|
||||
|
||||
|
||||
Reference in New Issue
Block a user