
    g5j                     .   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d0d"Zd1d$Z d2d&Z!dS )3ua  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_CACHEreturnboolc                     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     >/home/rurouni/.hermes/hermes-agent/hermes_cli/managed_scope.py_under_pytestr   )   s     !BJ..r   Optional[Path]c                    t           j                            dd                                          } | r't	          |           }|                                r|ndS t                      rdS t                                          rt          n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~     z~~2B77==??H )NNHHJJ(qqD( t#7#>#>#@#@JdJr   Nonec                     t           5  t                                           t                                           ddd           dS # 1 swxY w Y   dS )z@Drop cached managed config/env. For tests and post-edit reloads.N)_CACHE_LOCKr   clearr	   r   r   r   invalidate_managed_cacher#   J   s    	                   s   3AAApathr   cachec                   	 |                                  }n# t          $ r Y dS w xY w|j        |j        f}t	          |           }t
          5  |                    |          }|4|dd         |k    r&t          j        |d                   cddd           S ddd           n# 1 swxY w Y   	 t          | d          5 } ||          }ddd           n# 1 swxY w Y   n4# t          $ r'}	t                              d| |	           Y d}	~	dS d}	~	ww xY wt
          5  |d         |d         t          j        |          f||<   ddd           n# 1 swxY w Y   |S )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)
r$   r%   parsestkeypath_keyhitfparsedexcs
             r   _cached_readr=   Q   sh   YY[[   tt>2:
&C4yyH	 ) )ii!!?s2A2w#~~=Q(() ) ) ) ) ) ) ) ) ) ) ) ) ) ) ) ) ) ) ) ) ) )
$))) 	QU1XXF	 	 	 	 	 	 	 	 	 	 	 	 	 	 	   Q		
 	
 	
 ttttt 
 B Bq63q64=+@+@AhB B B B B B B B B B B B B B BMsl    
%%?B$$B(+B(0C% CC% CC%  C!C% %
D/DD 'EEEdictc                     t                      } | i S t          | dz  t          d           }t          |t                    r|ni S )zDParsed managed config.yaml, or {} when absent/malformed (fail-open).Nzconfig.yamlc                .    t          j        |           pi S )N)yaml	safe_load)r:   s    r   <lambda>z%load_managed_config.<locals>.<lambda>{   s    $.##)r r   )r   r=   r   
isinstancer>   managed_dirr;   s     r   load_managed_configrG   s   sS    !##K	m#)) F
  --56625r   Dict[str, str]c                     t                      } | i S t          | dz  t          t                    }t	          |t
                    r|ni S )z?Parsed managed .env (KEY=VALUE), or {} when absent (fail-open).Nz.env)r   r=   r	   
_parse_envrD   r>   rE   s     r   load_managed_envrK      sG    !##K	+.
JGGF--56625r   configc                `   	 t                      }|s| S ddlm}m}m}  | ||                    }t          |                    d          t                    rt          |          }d|d         i|d<    || |          S # t          $ r! t                              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)rG   hermes_cli.configrN   rO   rP   rD   r   r.   r>   r2   r3   r4   )rL   managedrN   rO   rP   managed_expandeds         r   apply_managed_overlayrW      s    (%'' 	M__________556F6Fw6O6OPP &**733S99 	O#$455)24DW4M(NW%{6#3444   FQUVVVs   B A-B (B-,B-c                   i }| D ]}|                                 }|r|                    d          sd|vr2|                    d          \  }}}|                                                      d          ||                                 <   |S )N#=z"')r   
startswith	partition)r:   outliner7   _values         r   rJ   rJ      s    C 6 6zz|| 	ts++ 	s$s++Q ;;==..u55CIIKKJr   r   dprefixr.   setc                   t                      }|                                 D ]]\  }}|r| d| nt          |          }t          |t                    r|r|t          ||          z  }H|                    |           ^|S )N.)rc   itemsr.   rD   r>   _flatten_keysadd)ra   rb   keyskvdotteds         r   rg   rg      s    D		  1$*6FQAa 	1 	M!V,,,DDHHVKr   c                 8    t          t                                S )zGDotted leaf keys pinned by the managed config (e.g. {'model.default'}).)rg   rG   r   r   r   managed_config_keysrn      s    ,..///r   
dotted_keyc                "    | t                      v S )zCTrue if the exact dotted config key is pinned by the managed layer.)rn   )ro   s    r   is_key_managedrq      s    ,....r   namec                "    | t                      v S )z=True if the env var name is pinned by the managed .env layer.)rK   )rr   s    r   is_env_managedrt      s    #%%%%r   )r
   r   )r
   r   )r
   r   )r$   r   r%   r   )r
   r>   )r
   rH   )rL   r>   r
   r>   )r   )ra   r>   rb   r.   r
   rc   )r
   rc   )ro   r.   r
   r   )rr   r.   r
   r   )"__doc__
__future__r   r/   loggingr   	threadingpathlibr   typingr   r   rA   	getLogger__name__r3   r   Lockr!   r   __annotations__r	   r   r   r#   r=   rG   rK   rW   rJ   rg   rn   rq   rt   r   r   r   <module>r      s    " # " " " " "   				           ! ! ! ! ! ! ! ! 		8	$	$ tM** in"$ $ $ $ $!
 ! ! ! !/ / / /K K K K,      D
6 
6 
6 
66 6 6 6( ( ( (V       0 0 0 0
/ / / /
& & & & & &r   