"""Governance consent + first-run filesystem bootstrap. v0.3 PR #7 extends this module to provision the user-wide skeleton on first run: ``/MYDEEPAGENT.md``, ``/global/memory/MEMORY.md``, ``/skills/``, ``/projects/``. All steps are idempotent so repeated calls do nothing destructive. The bootstrap is invoked at REPL/API startup so users always see the dirs even before they touch a `/remember` or `/skill` slash. """ from __future__ import annotations import json import logging import os from datetime import UTC, datetime from pathlib import Path from .config import Config from .errors import MyDeepAgentError from .instructions import ensure_global_instructions_initialized from .memory import ensure_memory_initialized, global_memory_dir from .skills import ensure_skills_initialized, user_skills_dir _LOG = logging.getLogger(__name__) def consent_path(data_dir: Path) -> Path: return data_dir / "governance-accepted.json" def has_consent(data_dir: Path) -> bool: return consent_path(data_dir).is_file() def record_consent(data_dir: Path) -> None: data_dir.mkdir(parents=True, exist_ok=True) target = consent_path(data_dir) payload = {"accepted_at": datetime.now(UTC).isoformat(timespec="seconds")} tmp = target.with_suffix(target.suffix + ".tmp") fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) try: os.write(fd, json.dumps(payload, indent=2).encode("utf-8")) os.fsync(fd) finally: os.close(fd) os.replace(tmp, target) def require_consent(data_dir: Path) -> None: if not has_consent(data_dir): raise MyDeepAgentError.human_required( "governance_not_accepted", message="governance consent not recorded", recovery_hint="run `mydeepagent init` and accept the data-governance prompt", ) def bootstrap_user_dirs(config: Config) -> None: """Provision the full user-wide skeleton. Idempotent. Creates (if missing): - ``/MYDEEPAGENT.md`` (global instructions w/ template) - ``/global/memory/MEMORY.md`` (empty index for cross-project memory) - ``/skills/`` (user-wide skills root) - ``/projects/`` (parent of per-project subtrees) Per-project subdirs (``projects//memory|skills``) are still created lazily by :class:`InteractiveSession` since they depend on the repo path; the parent ``projects/`` is materialised here so users see the expected layout even before opening their first session. """ data_dir = Path(config.data_dir) data_dir.mkdir(parents=True, exist_ok=True) ensure_global_instructions_initialized(config) ensure_memory_initialized(global_memory_dir(config)) ensure_skills_initialized(user_skills_dir(config)) (data_dir / "projects").mkdir(parents=True, exist_ok=True)