diff --git a/core/framework/skills/installer.py b/core/framework/skills/installer.py index 5c9a20f1..c4da0710 100644 --- a/core/framework/skills/installer.py +++ b/core/framework/skills/installer.py @@ -15,26 +15,17 @@ import subprocess import tempfile from pathlib import Path +from framework.config import HIVE_HOME from framework.skills.parser import ParsedSkill from framework.skills.skill_errors import SkillError, SkillErrorCode +# Default install destination for user-scope skills. +# Anchored on HIVE_HOME so the desktop shell can override the install +# root via $HIVE_HOME without patching every call site. +USER_SKILLS_DIR = HIVE_HOME / "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 - - return HIVE_HOME / "skills" - - -def _install_notice_sentinel() -> Path: - from framework.config import HIVE_HOME - - return HIVE_HOME / ".install_notice_shown" +# Sentinel file for the one-time security notice on first install (NFR-5). +INSTALL_NOTICE_SENTINEL = HIVE_HOME / ".install_notice_shown" _INSTALL_NOTICE = """\ @@ -60,13 +51,12 @@ def maybe_show_install_notice() -> None: Touches a sentinel file in $HIVE_HOME after showing the notice so it is only displayed once across all future installs. """ - sentinel = _install_notice_sentinel() - if sentinel.exists(): + if INSTALL_NOTICE_SENTINEL.exists(): return print(_INSTALL_NOTICE, flush=True) try: - sentinel.parent.mkdir(parents=True, exist_ok=True) - sentinel.touch() + INSTALL_NOTICE_SENTINEL.parent.mkdir(parents=True, exist_ok=True) + INSTALL_NOTICE_SENTINEL.touch() except OSError: pass # If we can't write the sentinel, just show the notice every time @@ -107,7 +97,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, @@ -208,7 +198,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: diff --git a/core/framework/skills/trust.py b/core/framework/skills/trust.py index de78c566..4d2e9e7d 100644 --- a/core/framework/skills/trust.py +++ b/core/framework/skills/trust.py @@ -20,6 +20,7 @@ from enum import StrEnum from pathlib import Path from urllib.parse import urlparse +from framework.config import HIVE_HOME from framework.skills.parser import ParsedSkill logger = logging.getLogger(__name__) @@ -30,17 +31,11 @@ _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" +# Persisted store of trusted git remotes (one-shot consent per repo). +_TRUSTED_REPOS_PATH = HIVE_HOME / "trusted_repos.json" -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" +# Sentinel for the one-time security notice (NFR-5). +_NOTICE_SENTINEL_PATH = HIVE_HOME / ".skill_trust_notice_shown" # --------------------------------------------------------------------------- @@ -59,7 +54,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 @@ -426,7 +421,7 @@ class TrustGate: def _maybe_show_security_notice(self, Colors) -> None: # noqa: N803 """Show the one-time security notice if not already shown (NFR-5).""" - sentinel = _notice_sentinel_path() + sentinel = _NOTICE_SENTINEL_PATH if sentinel.exists(): return self._print("") diff --git a/core/framework/tools/queen_lifecycle_tools.py b/core/framework/tools/queen_lifecycle_tools.py index 0013cd2e..fd237aa3 100644 --- a/core/framework/tools/queen_lifecycle_tools.py +++ b/core/framework/tools/queen_lifecycle_tools.py @@ -1326,6 +1326,16 @@ def register_queen_lifecycle_tools( # the entries' template ids can be threaded into the spawn data # (workers' ctx.picked_up_from references them). This mirrors the # plan §5d "auto-populated by run_parallel_workers" behavior. + # Preserve the task text in spec["data"] before any template-store + # mutation. Once spec["data"] is non-empty, spawn()'s + # ``input_data or {"task": task}`` fallback no longer fires, so the + # task description would otherwise vanish from the worker's first + # user message. Hoisted out of the try below so a non-fatal template + # failure cannot drop task text from the spawn payload. + for spec in normalised: + spec["data"] = dict(spec.get("data") or {}) + spec["data"].setdefault("task", spec["task"]) + _template_ids: list[int | None] = [None] * len(normalised) try: from framework.tasks import TaskListRole, get_task_store @@ -1343,7 +1353,6 @@ def register_queen_lifecycle_tools( _template_ids[i] = rec.id # Thread the template id into the worker's spawn data so # ColonyRuntime.spawn populates ctx.picked_up_from correctly. - spec["data"] = dict(spec.get("data") or {}) spec["data"]["__template_task_id"] = rec.id except Exception: logger.warning(