
    }-ji                     *   U d Z ddlZddlZddlZddlZddlZddlZddlZddlm	Z	  ej
        e          Z ej        d          Zg dZg dZda ej                    Z e            aee         ed<    ej                    Zd	Zd
e	ddfdZd
e	de	fdZde	fdZ e            ZdefdZdhZ ee         ed<    ej                    Z!da"dgdZ#dede$fdZ%de$fdZ&defdZ'deddfdZ(dgdZ)dede	fdZ*de	fdZ+ ej                    Z,de-fdZ.dgdZ/dgdZ0de1de	fdZ2dgd Z3 G d! d"          Z4 G d# d$          Z5dede	fd%Z6i d&d'd(d'd)d*d+d,d-d.d/d0d1d2d3d4d5d6d7d8d9d:d;d<d=d>d?d@dAdBdCdDdEdFdGdHdIdJdKdLdMdNdOZ7defdPZ8d
e	de1eef         fdQZ9d
e	fdRZ:d
e	fdSZ;dgdTZ<dUdVdedWe$de1fdXZ=de>fdYZ?de1fdZZ@defd[ZAde	fd\ZBdede	fd]ZC	 	 dhded^ed_e$de	fd`ZDdidae	dbedceddfddZE	 	 	 	 djded^ed_e$dbedcede1fdeZFdede1fdfZGdS )ka  
Hermes Web UI -- Profile state management.
Wraps hermes_cli.profiles to provide profile switching for the web UI.

The web UI maintains a process-level "active profile" that determines which
HERMES_HOME directory is used for config, skills, memory, cron, and API keys.
Profile switches update os.environ['HERMES_HOME'] and monkey-patch module-level
cached paths in hermes-agent modules (skills_tool, skill_manager_tool,
cron/jobs) that snapshot HERMES_HOME at import time.
    N)Pathz^[a-z0-9][a-z0-9_-]{0,63}$)memoriessessionsskillsskinslogsplans	workspacecron)config.yaml.envzSOUL.mddefault_loaded_profile_env_keys)ztools.skills_toolztools.skill_manager_toolhomereturnc                     t           D ]a}t          j                            |          }|$	 | |_        | dz  |_        7# t          $ r t                              d|           Y ^w xY wdS )zCPatch imported skill modules that cache HERMES_HOME at import time.Nr   zFailed to patch %s module)	_SKILL_HOME_MODULESsysmodulesgetHERMES_HOME
SKILLS_DIRAttributeErrorloggerdebug)r   module_namemodules      "/root/hermes-webui/api/profiles.pypatch_skill_home_modulesr   ,   s    * C C-->	C!%F $xF 	C 	C 	CLL4kBBBBB	CC Cs   ?%A'&A'c                 >    | j         j        dk    r| j         j         S | S )zGReturn the base Hermes home when *home* is already a named profile dir.profiles)parentnamer   s    r   _unwrap_profile_home_to_baser%   9   s"    {:%%{!!K    c                     t          j        dd                                          } | r.t          t	          |                                                     S t          j        dd                                          }|r0t	          |                                          }t          |          S t	          j                    dz  S )u/  Return the BASE ~/.hermes directory — the root that contains profiles/.

    This is intentionally distinct from HERMES_HOME, which tracks the *active
    profile's* home and changes on every profile switch.  The base dir must
    always point to the top-level .hermes regardless of which profile is active.

    Resolution order:
      1. HERMES_BASE_HOME env var (set explicitly, highest priority)
      2. HERMES_HOME env var — but only if it does NOT look like a profile subdir
         (i.e. its parent is not named 'profiles').  This handles test isolation
         where HERMES_HOME is set to an isolated test state dir.
      3. ~/.hermes (always-correct default)

    The bug this prevents: if HERMES_HOME has already been mutated to
    /home/user/.hermes/profiles/webui (by init_profile_state at startup),
    reading it here would make _DEFAULT_HERMES_HOME point to that subdir,
    causing switch_profile('webui') to look for
    /home/user/.hermes/profiles/webui/profiles/webui — which doesn't exist.

    HERMES_BASE_HOME normally points at the base home already, but isolated
    single-profile WebUI deployments can provide /base/profiles/<name> there as
    well.  Normalize both env vars through the same helper so active-profile
    and per-request resolution share one base-root contract (#749).
    HERMES_BASE_HOME r   z.hermes)osgetenvstripr%   r   
expanduserr   )base_overridehermes_homeps      r   _resolve_base_hermes_homer1   @   s    4 I0"55;;==M N+D,?,?,J,J,L,LMMM)M2..4466K /((**+A...9;;""r&   c                      t           dz  } |                                 rX	 |                     d                                          }|r|S n*# t          $ r t
                              d           Y nw xY wdS )z=Read the sticky active profile from ~/.hermes/active_profile.active_profileutf-8encodingz"Failed to read active profile filer   )_DEFAULT_HERMES_HOMEexists	read_textr,   	Exceptionr   r   )ap_filer#   s     r   _read_active_profile_filer<   i   s    "%55G~~ ?	?$$g$66<<>>D  	? 	? 	?LL=>>>>>	?9s   +A $A43A4_root_profile_name_cacheFc                      t           5  t                                           t                              d           daddd           dS # 1 swxY w Y   dS )zDrop the memoized root-profile-name set.

    Called whenever profile metadata might have changed: create, clone,
    delete, rename. The next _is_root_profile() call repopulates from
    list_profiles_api().
    r   FN)_root_profile_name_cache_lockr=   clearadd_root_profile_name_cache_loaded r&   r   _invalidate_root_profile_cacherD      s     
' 0 0 &&((( $$Y///*/'0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0s   6AAAr#   c                    | sdS | dk    rdS t           5  t          r| t          v cddd           S 	 ddd           n# 1 swxY w Y   	 t                      }n-# t          $ r  t
                              dd           Y dS w xY wt           5  t                                           t                              d           |D ]d}	 |	                    d          r5|	                    d          r t                              |d                    N# t          t          f$ r Y aw xY wda| t          v cddd           S # 1 swxY w Y   dS )	a  True if *name* resolves to the Hermes Agent root profile (~/.hermes).

    Matches the legacy 'default' alias plus any name where list_profiles_api()
    reports is_default=True. Memoized; call _invalidate_root_profile_cache()
    after mutating profile metadata.
    Fr   TNz/Failed to list profiles for root-profile lookup)exc_info
is_defaultr#   )r?   rB   r=   list_profiles_apir:   r   r   r@   rA   r   r   	TypeError)r#   infosr0   s      r   _is_root_profilerK      s     uyt	& 4 4* 	4334 4 4 4 4 4 4 4	44 4 4 4 4 4 4 4 4 4 4 4 4 4 4
!##   FQUVVVuu 
' 
0 
0 &&((( $$Y/// 	 	A55&& <155== <,006;;;"I.   *.'//
0 
0 
0 
0 
0 
0 
0 
0 
0 
0 
0 
0 
0 
0 
0 
0 
0 
0sX   >AA
A &BB8D?A
DD?D%"D?$D%%D??EEc                 f    | pd}|pd}||k    rdS t          |          rt          |          rdS dS )uD  Return True if a session/project row's profile matches the active profile.

    Treats both the literal alias 'default' and any renamed-root display name
    (per _is_root_profile) as equivalent, so legacy rows tagged 'default'
    still surface when the user has renamed the root profile to e.g. 'kinni',
    and vice versa.

    A row with no profile (`None` or empty string) is treated as belonging to
    the root profile — that's the convention used by the legacy backfill at
    api/models.py::all_sessions, and matches the default seen in
    `static/sessions.js` (`S.activeProfile||'default'`).

    Originally lived in api/routes.py; relocated here so both routes.py and
    out-of-process consumers (mcp_server.py) can import the canonical helper
    instead of duplicating the body. See #1614 for the visibility model.
    r   TF)rK   )row_profiler3   rowactives       r   _profiles_matchrP      sP    " 
"C(yF
f}}t !1&!9!9 t5r&   c                  D    t          t          dd          } | | S t          S )u   Return the currently active profile name.

    Priority:
      1. Thread-local (set per-request from hermes_profile cookie) — issue #798
      2. Process-level default (_active_profile)
    profileN)getattr_tls_active_profile)tls_names    r   get_active_profile_namerW      s&     tY--Hr&   c                     | t           _        dS )a  Set the per-request profile context for this thread.

    Called by server.py at the start of each request when a hermes_profile
    cookie is present.  Always paired with clear_request_profile() in a
    finally block so the thread-local is released after the request.
    NrT   rR   r#   s    r   set_request_profiler[      s     DLLLr&   c                      dt           _        dS )zClear the per-request profile context for this thread.

    Called by server.py in the finally block of do_GET / do_POST.
    Safe to call even if set_request_profile() was never called.
    NrY   rC   r&   r   clear_request_profiler]      s     DLLLr&   c                     | rt          |           rt          S t                              |           st          S t	          |           S )a  Resolve a logical profile name to its Hermes home path.

    Root/default aliases resolve to _DEFAULT_HERMES_HOME.  Valid named profiles
    resolve to _DEFAULT_HERMES_HOME/profiles/<name> even when the directory has
    not been created yet; the agent layer may create it on first use.  Invalid
    names fall back to the base home so traversal-shaped cookie values cannot
    influence filesystem paths.
    )rK   r7   _PROFILE_ID_RE	fullmatch_resolve_named_profile_homerZ   s    r   _resolve_profile_home_for_namerb      sL      $#D)) $####D)) $##&t,,,r&   c                  8    t          t                                S )zReturn the HERMES_HOME path for the currently active profile.

    Uses get_active_profile_name() so per-request TLS context (issue #798)
    is respected, not just the process-level global.
    )rb   rW   rC   r&   r   get_active_hermes_homerd   	  s     **A*C*CDDDr&   c                  L    t          t          t          dd          pd          S )Ncron_profile_depthr   )intrS   rT   rC   r&   r   _cron_profile_context_depthrh   '  s"    wt1155:;;;r&   c                  <    t                      dz   t          _        d S )N   )rh   rT   rf   rC   r&   r    _push_cron_profile_context_depthrk   +  s    9;;a?Dr&   c                  \    t                      } t          d| dz
            t          _        d S )Nr   rj   )rh   maxrT   rf   )depths    r   _pop_cron_profile_context_depthro   /  s)    '))E!!UQY//Dr&   jobc                 <   t          | pi                     d          pd                                          }|st                      S t	          |          rt
          S t                              |          s@t          	                    d| pi                     dd          |           t                      S t          |          }|                                s@t          	                    d| pi                     dd          |           t                      S |S )a  Resolve the profile home an auto-fired scheduler job should execute in.

    Legacy jobs with no profile keep the scheduler's server-default profile.
    Jobs pinned to a named profile execute under that profile's HERMES_HOME, so
    an in-process WebUI scheduler thread does not leak process-global config or
    .env into the agent run. If a profile was deleted after the job was saved,
    fall back to the server default rather than crashing every scheduler tick.
    rR   r)   zBCron job %s has invalid profile %r; falling back to server defaultid?zICron job %s references missing profile %r; falling back to server default)strr   r,   rd   rK   r7   r_   r`   r   warningra   is_dir)rp   rawr   s      r   _home_for_scheduled_cron_jobrx   4  s    sybooi((.B
/
/
5
5
7
7C (%''' $####C(( (PYBOOD#&&	
 	
 	
 &'''&s++D;;== (WYBOOD#&&	
 	
 	
 &'''Kr&   c                      	 ddl m}  n+# t          $ r t                              d           Y dS w xY wt          | dd          t          dd          rdS fd}d|_        |_        || _        dS )	a  Patch cron.scheduler.run_job for WebUI in-process scheduler safety.

    Standard WebUI deployments do not start the scheduler thread in-process, but
    if a future/single-process deployment calls cron.scheduler.tick() from the
    WebUI worker, tick's background job path has no request TLS context. Wrap
    run_job so each auto-fired job's persisted ``profile`` field gets the same
    HERMES_HOME isolation as the manual /api/crons/run path.
    r   NzDinstall_cron_scheduler_profile_isolation: cron.scheduler unavailablerun_job_webui_profile_isolatedFc                     t                      dk    r | g|R i |S t          t          |                     5   | g|R i |cd d d            S # 1 swxY w Y   d S )Nr   )rh   cron_profile_context_for_homerx   )rp   argskwargsoriginals      r   _webui_profile_isolated_run_jobzQinstall_cron_scheduler_profile_isolation.<locals>._webui_profile_isolated_run_jobe  s     '((1,,8C1$111&111*+G+L+LMM 	2 	28C1$111&11	2 	2 	2 	2 	2 	2 	2 	2 	2 	2 	2 	2 	2 	2 	2 	2 	2 	2s   AAAT)	cron.scheduler	schedulerImportErrorr   r   rS   r{   _webui_original_run_jobrz   )_csr   r   s     @r   (install_cron_scheduler_profile_isolationr   R  s    $$$$$$$   [\\\ sIt,,H78-FNN2 2 2 2 2 ?C#;>F#;1CKKKs   
 $22c                   *    e Zd ZdZdefdZd Zd ZdS )r}   a	  Context manager that pins HERMES_HOME to an explicit profile home path.

    Use this variant from worker threads that don't have TLS context (e.g. the
    background thread started by /api/crons/run). The HTTP-side variant below
    resolves the home via TLS.
    r   c                 .    t          |          | _        d S )N)r   _home)selfr   s     r   __init__z&cron_profile_context_for_home.__init__{  s    $ZZ


r&   c                    t                                            t                       	 t          j                            d          | _        t          | j                  t          j        d<   d | _	        	 dd l
m} |j        |j        |j        |j        f| _	        | j        |_        | j        dz  |_        |j        dz  |_        |j        dz  |_        n1# t           t"          f$ r t$                              d           Y nw xY wd | _        	 dd lm} t/          |dd           t/          |dd           t/          |d	d           f| _        | j        |_        | j        dz  |_        |j        d
z  |_        n1# t           t"          f$ r t$                              d           Y nw xY wn6# t6          $ r) t9                       t                                             w xY w| S )Nr   r   r   	jobs.jsonoutputz4cron_profile_context_for_home: cron.jobs unavailable_hermes_home	_LOCK_DIR
_LOCK_FILE
.tick.lockz9cron_profile_context_for_home: cron.scheduler unavailable)_cron_env_lockacquirerk   r*   environr   	_prev_envrt   r   _prev_cj	cron.jobsjobs
HERMES_DIRCRON_DIR	JOBS_FILE
OUTPUT_DIRr   r   r   r   _prev_csr   r   rS   r   r   r   r:   ro   release)r   _cjr   s      r   	__enter__z'cron_profile_context_for_home.__enter__~  s      (***%	Z^^M::DN(+DJBJ}% !DMU''''''!$s}cn ]!%#zF2 #{ :!$!80 U U USTTTTTU !DMZ,,,,,,C66Cd33Ct44!
 $(:  $
V 3!$!=0 Z Z ZXYYYYYZ 	 	 	+---""$$$	 sV   AF( 6AC F( +D F( D
F( A'E6 5F( 6+F$!F( #F$$F( (3Gc                 H   	 | j         !t          j                            dd            n| j         t          j        d<   | j        ?	 dd lm} | j        \  |_        |_        |_	        |_
        n# t          t          f$ r Y nw xY wt          | dd           9	 dd lm} | j        \  |_        |_        |_        n# t          t          f$ r Y nw xY wt)                       t*                                           n,# t)                       t*                                           w xY wdS Nr   r   r   Fr   r*   r   popr   r   r   r   r   r   r   r   r   rS   r   r   r   r   r   r   ro   r   r   r   exc_typeexc_valexc_tbr   r   s         r   __exit__z&cron_profile_context_for_home.__exit__  sI   	%~%
}d3333,0N
=)}(++++++RVR_OCNCL#-#^4   DtZ..:000000FJmCC$cmS^^#^4   D ,---""$$$$ ,---""$$$$uT   AC6 &A- ,C6 -B>C6  BC6  B7 6C6 7CC6 
CC6 6)DN)__name__
__module____qualname____doc__r   r   r   r   rC   r&   r   r}   r}   s  sX          T        ) ) )V    r&   r}   c                       e Zd ZdZd Zd ZdS )cron_profile_contexta`  Context manager that pins HERMES_HOME to the TLS-active profile.

    Usage:
        with cron_profile_context():
            from cron.jobs import list_jobs
            jobs = list_jobs(include_disabled=True)

    Serializes cron API calls across profiles (cron API is low-frequency;
    serialization cost is negligible compared to correctness).
    c                    t                                            t                       	 t          j                            d          | _        t                      }t          |          t          j        d<   d | _	        	 dd l
m} |j        |j        |j        |j        f| _	        ||_        |dz  |_        |j        dz  |_        |j        dz  |_        n1# t           t"          f$ r t$                              d           Y nw xY wd | _        	 dd lm} t/          |dd           t/          |dd           t/          |d	d           f| _        ||_        |dz  |_        |j        d
z  |_        n1# t           t"          f$ r t$                              d           Y nw xY wn6# t6          $ r) t9                       t                                             w xY w| S )Nr   r   r   r   r   z9cron_profile_context: cron.jobs unavailable; env-var onlyr   r   r   r   z>cron_profile_context: cron.scheduler unavailable; env-var only)r   r   rk   r*   r   r   r   rd   rt   r   r   r   r   r   r   r   r   r   r   r   r   r   r   rS   r   r   r   r:   ro   r   )r   r   r   r   s       r   r   zcron_profile_context.__enter__  s      (***$	Z^^M::DN)++D(+D		BJ}% !DMZ''''''!$s}cn ]!%#f} #{ :!$!80 Z Z ZXYYYYYZ !DM_,,,,,,C66Cd33Ct44!
 $(  $v!$!=0 _ _ _]^^^^^_ 	 	 	+---""$$$	 sV   AF ?AC F +D?F D
F AE+ *F ++FF FF 3Gc                 H   	 | j         !t          j                            dd            n| j         t          j        d<   | j        ?	 dd lm} | j        \  |_        |_        |_	        |_
        n# t          t          f$ r Y nw xY wt          | dd           9	 dd lm} | j        \  |_        |_        |_        n# t          t          f$ r Y nw xY wt)                       t*                                           n,# t)                       t*                                           w xY wdS r   r   r   s         r   r   zcron_profile_context.__exit__  sK   	%~%
}d3333,0N
=) }(++++++RVR_OCNCL#-#^4   DtZ..:000000FJmCC$cmS^^#^4   D ,---""$$$$ ,---""$$$$ur   N)r   r   r   r   r   r   rC   r&   r   r   r     s=        	 	( ( (T    r&   r   c                      t          |           S )u  Return the HERMES_HOME Path for *name* without mutating any process state.

    Safe to call from per-request context (streaming, session creation) because
    it reads only the filesystem — it never touches os.environ, module-level
    cached paths, or the process-level _active_profile global.

    Falls back to _DEFAULT_HERMES_HOME (same as 'default') when *name* is None,
    empty, 'default', or does not match the profile-name format (rejects path
    traversal such as '../../etc').
    )rb   rZ   s    r   get_hermes_home_for_profiler     s     *$///r&   backendTERMINAL_ENVenv_typecwdTERMINAL_CWDtimeoutTERMINAL_TIMEOUTlifetime_secondsTERMINAL_LIFETIME_SECONDS
modal_modeTERMINAL_MODAL_MODEdocker_imageTERMINAL_DOCKER_IMAGEdocker_forward_envTERMINAL_DOCKER_FORWARD_ENV
docker_envTERMINAL_DOCKER_ENVdocker_mount_cwd_to_workspace&TERMINAL_DOCKER_MOUNT_CWD_TO_WORKSPACEsingularity_imageTERMINAL_SINGULARITY_IMAGEmodal_imageTERMINAL_MODAL_IMAGEdaytona_imageTERMINAL_DAYTONA_IMAGEcontainer_cpuTERMINAL_CONTAINER_CPUcontainer_memoryTERMINAL_CONTAINER_MEMORYcontainer_diskTERMINAL_CONTAINER_DISKcontainer_persistentTERMINAL_CONTAINER_PERSISTENTTERMINAL_DOCKER_VOLUMESTERMINAL_PERSISTENT_SHELLTERMINAL_SSH_HOSTTERMINAL_SSH_USERTERMINAL_SSH_PORTTERMINAL_SSH_KEYTERMINAL_SSH_PERSISTENTTERMINAL_LOCAL_PERSISTENT)docker_volumespersistent_shellssh_hostssh_userssh_portssh_keyssh_persistentlocal_persistentc                     t          | t                    r| rdndS t          | t          t          f          rt	          j        |           S t          |           S )Ntruefalse)
isinstanceboollistdictjsondumpsrt   )values    r   _stringify_env_valuer   =  sU    % ,+vvG+%$&& !z%   u::r&   c                 \   t          |                                           } i }	 ddl}| dz  }|                                r)|                    |                    d                    ni }t          |t                    si }n# t          $ r i }Y nw xY wt          |t                    r|	                    di           ni }t          |t                    rCt                                          D ])\  }}||v r ||         t          ||                   ||<   *| dz  }|                                r	 |                    d                                          D ]}	|	                                }	|	r|	                    d          std	|	v rp|	                    d	d
          \  }
}|
                                }
|                                                    d                              d          }|
r|r|||
<   n+# t          $ r t"                              d|           Y nw xY w|S )a  Return env vars needed to run an agent turn for a profile home.

    WebUI profile switching is per-client/cookie scoped, so it intentionally
    does not call ``switch_profile(..., process_wide=True)`` for every browser.
    Agent/tool code still consumes terminal backend settings through
    environment variables (matching ``hermes -p <profile>``), so streaming must
    apply the selected profile's terminal config and ``.env`` for the duration
    of that run.
    r   Nr   r4   r5   terminalr   #=rj   "'z"Failed to read runtime env from %s)r   r-   yamlr8   	safe_loadr9   r   r   r:   r   _TERMINAL_ENV_MAPPINGSitemsr   
splitlinesr,   
startswithsplitr   r   )r   env_yamlcfg_pathcfgterminal_cfgkeyenv_keyenv_pathlinekvs               r   get_profile_runtime_envr  E  sa    ::  ""DC-'GOGXGX`eooh00'0BBCCC^`#t$$ 	C    /9d.C.CK377:r***L,%% G288:: 	G 	GLCl""|C'8'D3L4EFFGf}H I
	I **G*<<GGII # #zz|| # 4 4 #::c1--DAq		A		,,22377A #Q #!"A#  	I 	I 	ILL=xHHHHH	I Js%   AB BB6C
H %H)(H)c                    t          |           t          j        d<   t          |            	 ddlm} | |_        | dz  |_        |j        dz  |_        |j        dz  |_	        n1# t          t          f$ r t                              d           Y nw xY w	 ddlm} | |_        | dz  |_        |j        dz  |_        dS # t          t          f$ r t                              d	           Y dS w xY w)
zCSet HERMES_HOME env var and monkey-patch cached module-level paths.r   r   Nr   r   r   z Failed to patch cron.jobs moduler   z%Failed to patch cron.scheduler module)rt   r*   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   )r   r   r   s      r   _set_hermes_homer  s  s    #D		BJ}T"""9f}{20( 9 9 97888889>$$$$$$v5( > > ><======>s#   5A# #+BB&B= =+C,+C,c                 .   t          t                    D ]"}t          j                            |d           #t                      a| dz  }|                                sdS 	 t                      }|                    d                                          D ]}|	                                }|r|
                    d          sd|v r|                    dd          \  }}|	                                }|	                                	                    d          	                    d	          }|r&|r$|t          j        |<   |                    |           |adS # t          $ r- t                      at                              d
|           Y dS w xY w)a&  Load .env from the profile dir into os.environ with profile isolation.

    Clears env vars that were loaded from the previously active profile before
    applying the current profile's .env. This prevents API keys and other
    profile-scoped secrets from leaking across profile switches.
    Nr   r4   r5   r   r   rj   r   r   zFailed to reload dotenv from %s)r   r   r*   r   r   setr8   r9   r   r,   r   r   rA   r:   r   r   )r   r  r  loaded_keysr  r	  r
  s          r   _reload_dotenvr    s    ,-- " "

sD!!!!"uuf}H?? B #&&&88CCEE 	' 	'D::<<D 'DOOC00 'SD[[zz#q))1GGIIGGIIOOC((..s33 ' '$%BJqMOOA&&&#.    B B B#&55 6AAAAAABs   "C9E 3FFc                      t                      at                      } t          |            t	                       t          |            dS )zInitialize profile state at server startup.

    Reads ~/.hermes/active_profile, sets HERMES_HOME env var, patches
    module-level cached paths.  Called once from config.py after imports.
    N)r<   rU   rd   r  r   r  r$   s    r   init_profile_stater    sH     011O!##DT,...4r&   T)process_wider  c                   ddl m}m}m} |r=|5  t	          |          dk    rt          d          	 ddd           n# 1 swxY w Y   t          |           rt          }n6t          |           }|	                                st          d|  d          t          5  |r | at          |           t          |           ddd           n# 1 swxY w Y   |rh	 t          dz  }|                    t          |           rdn| d	
           n*# t           $ r t"                              d           Y nw xY w |             |rddl m}  |            }ns	 ddl}	|dz  }
|
                                r)|	                    |
                    d	
                    ni }t1          |t2                    si }n# t           $ r i }Y nw xY w|                    di           }d}t1          |t6                    r|}n*t1          |t2                    r|                    d          }d}	 ddl m} |dz  dz  }|                                r|                    d	
                                          }|rVt=          |                                          }|	                                r!t7          |                                           }|dD ]~}|                    |          }|ret=          t7          |                                                                                     }|	                                rt7          |          } n||                    di           }t1          |t2                    r|                    dd          }|rtt7          |          dvrct=          t7          |                                                                                     }|	                                rt7          |          }|t7          |          }nW# t           $ rJ 	 ddl m} t7          |          }n0# t           $ r# t7          t=          j!                              }Y nw xY wY nw xY wtE                      | ||dS )a#  Switch the active profile.

    Validates the profile exists, updates process state, patches module caches,
    reloads .env, and reloads config.yaml.

    Args:
        name: Profile name to switch to.
        process_wide: If True (default), updates the process-global
            _active_profile.  Set to False for per-client switches from the
            WebUI where the profile is managed via cookie + thread-local (#798).

    Returns: {'profiles': [...], 'active': name}
    Raises ValueError if profile doesn't exist or agent is busy.
    r   )STREAMSSTREAMS_LOCKreload_configzRCannot switch profiles while an agent is running. Cancel or wait for it to finish.N	Profile '' does not exist.r3   r)   r4   r5   z#Failed to write active profile file)
get_configr   modelr   )DEFAULT_WORKSPACEwebui_statezlast_workspace.txt)r
   default_workspacer   r   ).r)   )r!   rO   default_modelr  )#
api.configr  r  r  lenRuntimeErrorrK   r7   ra   rv   
ValueError_profile_lockrU   r  r  
write_textr:   r   r   r  r   r8   r   r9   r   r   r   rt   r  r,   r   r-   resolver   rH   )r#   r  r  r  r  r   r;   r  r  r  r  	model_cfgr!  r  _DWlw_file_p_pp_key_v_tc_cwd_DW2s                          r   switch_profiler3    s   $ @?????????   	 	7||a"7    	 	 	 	 	 	 	 	 	 	 	 	 	 	 	  B#*400{{}} 	B@@@@AAA	 ! ! 	!"OT"""4   ! ! ! ! ! ! ! ! ! ! ! ! ! ! !  		@*-==G%5d%;%;ErrPWXXXX 	@ 	@ 	@LL>?????	@ 	  ))))))jll	    m+HKS??K\K\d%//("4"4g"4"F"FGGGbdCc4((  	 	 	CCC	$$IM)S!! 1!	It	$	$ 1!i00  1777777&)==>> 	;""G"44::<<B ;2hh))++::<< ;(+CKKMM(:(:%$:  WWT]] s2ww--2244<<>>Czz|| ,/HH)$''*b))C#t$$ 5wwub)) 5CIIY66s4yy//4466>>@@Czz|| 5,/HH)$ #C 1 1 1	1<<<<<< #D		 	1 	1 	1 #DIKK 0 0	1	1 &''&.	  s}   #?AA#CCC2D $D54D5AF6 6GG%G/P 
Q) P65Q)6*Q# Q)"Q##Q)(Q)c                  L   	 ddl m}   |             }n# t          $ r t                      gcY S w xY wt	                      }g }|D ]_}|                    |j        t          |j                  |j	        |j        |k    |j
        |j        |j        |j        |j        d	           `|S )z>List all profiles with metadata, serialized for JSON response.r   )list_profiles	r#   pathrG   	is_activegateway_runningr  providerhas_envskill_count)hermes_cli.profilesr5  r   _default_profile_dictrW   appendr#   rt   r7  rG   r9  r  r:  r;  r<  )r5  rJ   rO   resultr0   s        r   rH   rH   E  s    )555555 ) ) )%''(((() %&&FF  FKK,6) 0W
y=

 

 
	 
	 
	 
	 Ms    //c            
      r    dt          t                    dddddt          dz                                  dd	S )z8Fallback profile dict when hermes_cli is not importable.r   TFNr   r   r6  )rt   r7   r8   rC   r&   r   r>  r>  _  sH     ()) (6199;;
 
 
r&   c                     | dk    rt          d          t                              |           st          d| d          dS )zDValidate profile name format (matches hermes_cli.profiles upstream).r   zFCannot create a profile named 'default' -- it is the built-in profile.zInvalid profile name z%. Must match [a-z0-9][a-z0-9_-]{0,63}N)r%  r_   r`   rZ   s    r   _validate_profile_namerC  n  sd    yabbb##D)) 
2D 2 2 2
 
 	

 
r&   c                  :    t           dz                                  S )z7Return the canonical root that contains named profiles.r!   )r7   r(  rC   r&   r   _profiles_rootrE  z  s     :-66888r&   c                     t          |            t                      }|| z                                  }|                    |           |S )zResolve a named profile to a directory under the profiles root.

    Validates *name* as a logical profile identifier first, then resolves the
    final filesystem path and enforces containment under ~/.hermes/profiles.
    )rC  rE  r(  relative_to)r#   profiles_root	candidates      r   ra   ra     sM     4   "$$M%..00I-(((r&   
clone_fromclone_configc                    t           dz  | z  }|                                rt          d|  d          |                    dd           t          D ]}||z                      dd           |ru|rst          |          rt           }nt           dz  |z  }|                                r;t          D ]3}||z  }|                                rt          j	        |||z             4|S )zKCreate a profile directory without hermes_cli (Docker/standalone fallback).r!   r  z' already exists.TFparentsexist_ok)
r7   r8   FileExistsErrormkdir_PROFILE_DIRSrK   rv   _CLONE_CONFIG_FILESshutilcopy2)r#   rJ  rK  profile_dirsubdir
source_dirfilenamesrcs           r   _create_profile_fallbackr[    s"    '3d:K CA$AAABBB dU333 B B	v	$$TD$AAAA  	>
 	>J'' 	H-JJ-
:ZGJ 	>/ > > 8+::<< >LkH&<===r&   rV  base_urlapi_keyc                 *   |s|sdS | dz  }	 ddl }n# t          $ r Y dS w xY wi }|                                rm	 |                    |                    d                    }t          |t                    r|}n+# t          $ r t          	                    d|           Y nw xY w|
                    di           }t          |t                    si }|r||d<   |r||d	<   ||d<   |                    |                    |d
d          d           dS )z<Write custom endpoint fields into config.yaml for a profile.Nr   r   r4   r5   zFailed to load config from %sr  r\  r]  FT)default_flow_styleallow_unicode)r   r   r8   r   r9   r   r   r:   r   r   r   r'  dump)rV  r\  r]  config_pathr  r  loadedmodel_sections           r   _write_endpoint_to_configre    st    G -K   
C G	G__[%:%:G%:%L%LMMF&$''  	G 	G 	GLL8+FFFFF	GGGGR((MmT**  -$,j! +#*i  CL5::ceSW:XXcjkkkkks    
  A A; ;%B#"B#c           
         t          |            |t          |          st          |           	 ddlm}  || ||dd           n!# t          $ r t          | ||           Y nw xY wt          dz  | z  }t                      D ]`}|d         | k    rR	 t          |	                    d	          p|          }n*# t          $ r t                              d
           Y nw xY w na|                    dd           t          |||           t                       t                      D ]}|d         | k    r|c S | t!          |          dt"          | k    ddd|dz                                  dd	S )z8Create a new profile. Returns the new profile info dict.Nr   )create_profileFT)rJ  rK  	clone_allno_aliasr!   r#   r7  zFailed to parse profile pathrM  )r\  r]  r   r6  )rC  rK   r=  rg  r   r[  r7   rH   r   r   r:   r   r   rQ  re  rD   rt   rU   r8   )r#   rJ  rK  r\  r]  rg  profile_pathr0   s           r   create_profile_apirk    s   
 4    &6z&B&Bz***
A666666!%	
 	
 	
 	
 	
  A A A z<@@@@@A (*4t;L    V9=#AEE&MM$A\BB = = =;<<<<<=E  td333lXwOOOO #$$$     V9HHH  L!!$,  6)1133
 
 
s#   A A&%A&$B88$CCc                    t          |           rt          d          t          |            t          | k    r2	 t	          d           n!# t
          $ r t          d|  d          w xY w	 ddlm}  || d           nh# t          $ r[ dd	l	}t          |           }|                                r |j        t          |                     nt          d
|  d          Y nw xY wt                       d| dS )zCDelete a profile. Switches to default first if it's the active one.z"Cannot delete the default profile.r   zCannot delete active profile 'z=' while an agent is running. Cancel or wait for it to finish.r   )delete_profileT)yesNr  r  )okr#   )rK   r%  rC  rU   r3  r$  r=  rm  r   rT  ra   rv   rmtreert   rD   )r#   rm  rT  rV  s       r   delete_profile_apirq    sj    ?=>>>4    $	9%%%% 	 	 	3 3 3 3  	
B666666t&&&&& B B B1$77 	BFM#k**++++@@@@AAA ,+B #$$$%%%s   A
 
A(,B   A"C%$C%)r   N)NF)NN)NFNN)Hr   r   loggingr*   rerT  r   	threadingpathlibr   	getLoggerr   r   compiler_   rR  rS  rU   Lockr&  r  r   rt   __annotations__localrT   r   r   r%   r1   r7   r<   r=   r?   rB   rD   r   rK   rP   rW   r[   r]   rb   rd   r   rg   rh   rk   ro   r   rx   r   r}   r   r   r   r   r  r  r  r  r3  r   rH   r>  rC  rE  ra   r[  re  rk  rq  rC   r&   r   <module>r{     s"  	 	 	   				 				  



          		8	$	$ 9::   988  	  %(SUU #c( * * * yG 
C4 
CD 
C 
C 
C 
Ct     $#4 $# $# $# $#L 1022 
3 
 
 
 
> '0[ #c( 0 0 0 .	 0 0 "' 0 0 0 0 03  04  0  0  0  0FD    6
 
 
 
 
c d       - - - - - - E E E E E6  !!<S < < < <@ @ @ @0 0 0 0
d t    <2 2 2 2BK K K K K K K K\N N N N N N N Nb0c 0d 0 0 0 0~ 
> !	
 3 ' + 7 ' $%M 5 ) - - 3  /!" ;#$ 03###!/33   :3    +$ +4S> + + + +\>4 > > > >2B B B B BD    7; F F F Ft Ft F F F FR4    4t    	
 	
 	
 	
 	
9 9 9 9 9

c 
d 
 
 
 
 ;?38 3 C ,0=A   6l l4 l3 lPS l_c l l l l8 59,1'+&*< <S <c <%)<!$< !$< 04< < < <~&S &T & & & & & &r&   