
    `gj                     .   U d Z ddlmZ ddlZddlZddlZddlZddlmZ ddl	m
Z
mZ ddlZ ej                  e      Z ed      Z ej"                         Zi Zded<   i Zded	<   dd
ZddZddZddZddZddZddZddZdddZddZd dZ d!dZ!y)"ua  Managed scope — IT-pushed, user-immutable config & env layer.

A system-level directory (default ``/etc/hermes``, root-owned and not
user-writable) supplies ``config.yaml`` and ``.env`` values that WIN over the
user's ``~/.hermes/config.yaml`` and ``~/.hermes/.env`` on a per-leaf-key basis.

This is DISTINCT from ``hermes_cli.config.is_managed()`` / ``HERMES_MANAGED``,
which is a coarse package-manager write-lock (declarative-distro / formula
installs). That lock blocks all mutation; this layer injects specific immutable
values. The two are independent and may coexist.

v1 enforcement is filesystem permissions only — see
``docs/design/managed-scope.md`` §7. v1 is Linux/POSIX-first; ``get_managed_dir()``
is the single seam for adding macOS / Windows native locations later.

Attribution: do not reference any third-party product by name in this file.
    )annotationsN)Path)DictOptionalz/etc/hermesDict[str, tuple]_CONFIG_CACHE
_ENV_CACHEc                 &    dt         j                  v S )al  True when running inside the test suite.

    Used to ignore the system default ``/etc/hermes`` during tests so a real
    managed scope on a developer/CI box can't leak policy into the suite. Tests
    that exercise managed scope set ``HERMES_MANAGED_DIR`` explicitly, which is
    still honored (the override path below runs before this guard takes effect).
    PYTEST_CURRENT_TEST)osenviron     K/root/.hermes/venv/lib/python3.12/site-packages/hermes_cli/managed_scope.py_under_pytestr   )   s     !BJJ..r   c                     t         j                  j                  dd      j                         } | rt	        |       }|j                         r|S dS t               ryt        j                         rt        S dS )uj  Resolve the managed-scope directory, or None when no scope is present.

    Resolution (highest priority first):
      1. ``$HERMES_MANAGED_DIR`` — deployment/bootstrap path override (IT-only;
         never persisted to any .env). Honored only when set to a non-empty value
         AND the directory exists.
      2. ``/etc/hermes`` — POSIX default, when it exists. Ignored under pytest so
         a real system managed scope can't leak into the test suite.

    A non-existent directory at either tier resolves to None (no managed scope),
    which is the common case and must be cheap + side-effect-free.
    HERMES_MANAGED_DIR N)r   r   getstripr   is_dirr   _DEFAULT_MANAGED_DIR)overrideps     r   get_managed_dirr   4   s`     zz~~2B7==?HNHHJq(D(#7#>#>#@JdJr   c                     t         5  t        j                          t        j                          ddd       y# 1 sw Y   yxY w)z@Drop cached managed config/env. For tests and post-edit reloads.N)_CACHE_LOCKr   clearr	   r   r   r   invalidate_managed_cacher   J   s2    	   s	   )9Ac                d   	 | j                         }|j                  |j                  f}t	        |       }t
        5  |j                  |      }|)|dd |k(  r!t        j                  |d         cddd       S ddd       	 t        | d      5 } ||      }ddd       t
        5  |d   |d   t        j                        f||<   ddd       |S # t        $ r Y yw xY w# 1 sw Y   ixY w# 1 sw Y   VxY w# t        $ r!}	t        j                  d| |	       Y d}	~	yd}	~	ww xY w# 1 sw Y   S xY w)uN  Shared (mtime_ns, size)-keyed read. Returns a deepcopy of the parsed value.

    Returns ``None`` when the file is absent or fails to parse (fail-open). A
    parse failure is logged LOUDLY — the admin needs to know their policy isn't
    being applied — but never raises, so a malformed managed file can't brick
    startup.
    N   zutf-8)encodingu   managed scope: failed to parse %s: %s — IGNORING this managed file. Admin policy from this file is NOT being applied. Fix and restart.r      )statOSErrorst_mtime_nsst_sizestrr   r   copydeepcopyopen	Exceptionloggerwarning)
pathcacheparsestkeypath_keyhitfparsedexcs
             r   _cached_readr9   Q   s2   YY[ >>2::
&C4yH	 )ii!?s2Aw#~==Q() ) )
$) 	Q1XF	 
 Bq63q64==+@AhBM+  ) )
	 	 Q		
 BMsX   C 3C C8 	C,C8 %"D%	CC C),C51C8 8	D"DD"%D/c                 p    t               } | i S t        | dz  t        d       }t        |t              r|S i S )zDParsed managed config.yaml, or {} when absent/malformed (fail-open).zconfig.yamlc                4    t        j                  |       xs i S )N)yaml	safe_load)r6   s    r   <lambda>z%load_managed_config.<locals>.<lambda>{   s    $..#)r r   )r   r9   r   
isinstancedictmanaged_dirr7   s     r   load_managed_configrC   s   sC    !#K	m#)F
  -6525r   c                 v    t               } | i S t        | dz  t        t              }t	        |t
              r|S i S )z?Parsed managed .env (KEY=VALUE), or {} when absent (fail-open).z.env)r   r9   r	   
_parse_envr?   r@   rA   s     r   load_managed_envrF      s<    !#K	+.
JGF-6525r   c                   	 t               }|s| S ddlm}m}m}  | ||            }t        |j                  d      t              rt        |      }d|d   i|d<    || |      S # t        $ r t        j                  dd       | cY S w xY w)u  Overlay administrator-pinned config values on top of an already-built dict.

    The single, shared way for any config loader that builds its own dict
    (rather than going through hermes_cli.config.load_config) to honor managed
    scope. Mirrors hermes_cli.config._load_config_impl's managed merge exactly:

      * expand the managed config's ``${VAR}`` refs against the PROCESS env only
        (never user-config-defined refs), so a user cannot shadow a managed
        literal via a ${VAR} they control;
      * normalize the managed config's root ``model`` key (a bare ``model: x/y``
        string is promoted to ``model.default``) so it can't clobber the dict
        shape callers expect;
      * leaf-level deep-merge managed ON TOP, so managed wins per-leaf while
        sibling keys stay user-controlled.

    Fail-open: returns ``config`` unchanged if no managed scope is present or on
    any error — managed scope must never break a caller's startup. Mutates and
    returns ``config`` (callers pass a dict they own).
    r   )_deep_merge_expand_env_vars_normalize_root_model_keysmodeldefaultz-managed scope: failed to apply config overlayT)exc_info)rC   hermes_cli.configrH   rI   rJ   r?   r   r(   r@   r,   r-   r.   )configmanagedrH   rI   rJ   managed_expandeds         r   apply_managed_overlayrR      s    (%'M__56Fw6OP &**73S9#$45)24DW4M(NW%6#344 FQUVs   A% AA% %"B
	B
c                    i }| D ]o  }|j                         }|r|j                  d      sd|vr+|j                  d      \  }}}|j                         j                  d      ||j                         <   q |S )N#=z"')r   
startswith	partition)r6   outliner3   _values         r   rE   rE      st    C 6zz|ts+s$s+Q ;;=..u5CIIK6 Jr   c                    t               }| j                         D ]L  \  }}|r| d| n
t        |      }t        |t              r|r|t        ||      z  }<|j                  |       N |S )N.)setitemsr(   r?   r@   _flatten_keysadd)dprefixkeyskvdotteds         r   r`   r`      si    D	 1$*F81QCAa1M!V,,DHHV Kr   c                 (    t        t                     S )zGDotted leaf keys pinned by the managed config (e.g. {'model.default'}).)r`   rC   r   r   r   managed_config_keysri      s    ,.//r   c                    | t               v S )zCTrue if the exact dotted config key is pinned by the managed layer.)ri   )
dotted_keys    r   is_key_managedrl      s    ,...r   c                    | t               v S )z=True if the env var name is pinned by the managed .env layer.)rF   )names    r   is_env_managedro      s    #%%%r   )returnbool)rp   zOptional[Path])rp   None)r/   r   r0   r   )rp   r@   )rp   zDict[str, str])rO   r@   rp   r@   )r   )rb   r@   rc   r(   rp   r^   )rp   r^   )rk   r(   rp   rq   )rn   r(   rp   rq   )"__doc__
__future__r   r)   loggingr   	threadingpathlibr   typingr   r   r<   	getLogger__name__r-   r   Lockr   r   __annotations__r	   r   r   r   r9   rC   rF   rR   rE   r`   ri   rl   ro   r   r   r   <module>r}      s   " #   	   ! 			8	$ M* inn"$ $!
 !/K,D
66(V0
/
&r   