§
    øžyj¡   ã                  ó.  — 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/ragecks/.hermes/hermes-agent/hermes_cli/managed_scope.pyÚ_under_pytestr   )   s   € ð !¥B¤JÐ.Ð.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�~Š~Ð2°BÑ7Ô7×=Ò=Ñ?Ô?€HØð )Ý�‰NŒNˆØ—H’H‘J”JÐ(ˆqˆq DÐ(Ý�„ð Øˆ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  € ðØ�YŠY‰[Œ[ˆˆøÝð ð ð ØˆtˆtðøøøàŒ>˜2œ:Ð
&€CÝ�4‰yŒy€HÝ	ð )ð )Ø�iŠi˜Ñ!Ô!ˆØˆ?˜s 2 A 2œw¨#š~˜~Ý”=  Q¤Ñ(Ô(ð)ð )ð )ð )ñ )ô )ð )ð )ð )ð )ð )ñ )ô )ð )ð )ð )ð )ð )ð )øøøð )ð )ð )ð )ð
Ý�$ Ð)Ñ)Ô)ð 	¨QØ�U˜1‘X”XˆFð	ð 	ð 	ñ 	ô 	ð 	ð 	ð 	ð 	ð 	ð 	øøøð 	ð 	ð 	ð 	øøåð ð ð Ý�ŠðQàØñ		
ô 	
ð 	
ð ˆtˆtˆtˆtˆtøøøøðøøøõ 
ð Bð BØ˜qœ6 3 q¤6­4¬=¸Ñ+@Ô+@Ð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õ
   ­Ñ-Ô-Ð5ˆ6ˆ6°2Ð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ÔG€FÝ ­Ñ-Ô-Ð5ˆ6ˆ6°2Ð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Ð5Ð6FÐ6FÀwÑ6OÔ6OÑPÔPÐõ Ð&×*Ò*¨7Ñ3Ô3µSÑ9Ô9ð 	OÝ#Ð$4Ñ5Ô5ÐØ)2Ð4DÀWÔ4MÐ(NÐ˜WÑ%Øˆ{˜6Ð#3Ñ4Ô4Ð4øÝð ð ð Ý�ŠÐFÐQUˆÑ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ˆØ�zŠz‰|Œ|ˆØð 	�t—’ sÑ+Ô+ð 	¨s¸$¨¨ØØŸš sÑ+Ô+‰ˆˆQ�Ø Ÿ;š;™=œ=×.Ò.¨uÑ5Ô5ˆˆC�IŠI‰KŒKÑÐØ€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Ø$*Ð6�F��˜Q���µ°A±´ˆÝ�a�ÑÔð 	 1ð 	Ø•M ! VÑ,Ô,Ñ,ˆDˆDà�HŠH�VÑÔÐÐØ€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Ñ	$Ô	$€ð �t˜MÑ*Ô*Ð àˆiŒnÑÔ€à"$€Ð $Ð $Ð $Ñ $Ø!€
Ð !Ð !Ð !Ñ !ð/ð /ð /ð /ðKð Kð Kð Kð,ð ð ð ðð ð ð ðD
6ð 
6ð 
6ð 
6ð6ð 6ð 6ð 6ð(ð (ð (ð (ðVð ð ð ðð ð ð ð ð0ð 0ð 0ð 0ð
/ð /ð /ð /ð
&ð &ð &ð &ð &ð &r   