
    nSjw8                       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
 ddlmZ dZ G d d	e          Z G d
 de          Z G d de          Z G d de          Z G d de          Z G d de          Ze G d d                      Ze G d d                      Z ej        d          Zd5dZd6d Zd7d#Zd8d%Zd9d'Zd:d)Zd;d-Zd<d/Z d=d0Z!d>d4Z"dS )?u#  Single resolver for every vision_analyze image source -> bytes + mime.

All source handling (data:/http(s)/file/local/container) funnels through
:func:`resolve_image_source` so size and magic-byte checks are enforced exactly
once.  Returns raw bytes (not a path): the downstream step is base64 -> data URL
(RFC 2397) and provider base64 content blocks.

Security (terminal-backend confinement, GHSA-gpxw-6wxv-w3qq): under a non-local
terminal backend the file tools are confined to the sandbox (SECURITY.md 2.2),
but vision read images host-side. This resolver enforces the same boundary:

  * local backend            -> read any host path (chosen posture, unchanged)
  * non-local backend:
      path in a media cache   -> host-read (the gateway/download caches live on
                                 the host and are bind-mounted into the sandbox)
      path anywhere else      -> read the bytes *inside the sandbox* via exec-read
                                 (the agent can already ``cat`` any container file;
                                 this stays within the sandbox boundary and never
                                 reaches the host's ``/etc/passwd`` / ``~/.ssh``).

So a prompt-injected ``vision_analyze('/etc/passwd')`` under Docker reads the
*container's* file (what every other tool sees), not the host's — no escape —
while container-only images (tmpfs ``/workspace``, root-owned) are still
deliverable. This is the unified delivery + confinement model: the same
mechanism that fixes "vision can't see container files" also closes the escape.
    )annotationsN)	dataclass)Path)Optionali   c                  (     e Zd Zdddd fdZ xZS )	ImageResolutionError srcoriginmessagestrr   r   c               h    t                                          |           ||c| _        | _        d S )N)super__init__r   r   )selfr   r   r   	__class__s       4/home/ice/.hermes/hermes-agent/tools/image_source.pyr   zImageResolutionError.__init__/   s/    !!! #V$+++    )r   r   r   r   r   r   )__name__
__module____qualname__r   __classcell__)r   s   @r   r   r   .   sL        35R , , , , , , , , , , , ,r   r   c                      e Zd ZdS )UnsupportedSchemeNr   r   r    r   r   r   r   4           Dr   r   c                      e Zd ZdS )SourceUnsafeNr   r   r   r   r    r    8   r   r   r    c                      e Zd ZdS )SourceTooLargeNr   r   r   r   r"   r"   <   r   r   r"   c                      e Zd ZdS )SourceNotFoundNr   r   r   r   r$   r$   @   r   r   r$   c                      e Zd ZdS )
NotAnImageNr   r   r   r   r&   r&   D   r   r   r&   c                      e Zd ZU dZded<   dS )ResolveContextNOptional[str]task_id)r   r   r   r*   __annotations__r   r   r   r(   r(   H   s#         !G!!!!!!r   r(   c                  .    e Zd ZU ded<   ded<   ded<   dS )ResolvedImagebytesdatar   mimer   N)r   r   r   r+   r   r   r   r-   r-   M   s+         KKKIIIKKKKKr   r-   z^[A-Za-z][A-Za-z0-9+.\-]*://r   r   ctxreturnc                T  K   t          | t                    r|                                 st          dt          |                     |                                 }|                    d          r$t          |          \  }}t          ||d|          S |                    d          rGt          |          }|rt          ||          t          t          |           d {V dd|          S t                              |          r8|                                                    d          st          d	|          |                                                    d          r|t          d          d          n|}t          t           j                            |                    }t'          ||          }||                                r	 d
dlm}	 n# t.          $ r d }	Y nw xY w|	K	  |	t          |                     n1# t0          $ r$}
t          t          |
          |d          d }
~
ww xY wt3          j        |j                   d {V }t          |dd|          S t9                      rt          d| d|d          t;          |||           d {V S )Nzimage_url is requiredr   data:r/   )zhttp://zhttps://r	   httpzfile://zgUnrecognized image source scheme. Use an http(s) URL, a local file path, a file:// URI, or a data: URL.r   )raise_if_read_blockedfiler
   zimage file not found: '')
isinstancer   stripr$   
startswith_resolve_data_url	_finalize_http_block_reasonr    _download_to_bytes
_SCHEME_REmatchlowerr   lenr   ospath
expanduser_permitted_host_read_targetis_fileagent.file_safetyr7   	Exception
ValueErrorasyncio	to_thread
read_bytes_is_local_terminal_backend_resolve_container_fallback)r   r1   sr/   r0   reason	candidatephost_targetr7   excs              r   resolve_image_sourcerX   Y   s     c3 Dsyy{{ D4#c((CCCC		A||G 0&q))
dtVQ///||+,, E#A&& 	.v1----1!44444444b&!DDD 
17799#7#7	#B#B 
8
 
 
 	
 '(ggii&:&:9&E&EL#i..//""1IRW	**++A .a55K;#6#6#8#8	)??????? 	) 	) 	)$(!!!	) ,C%%c+&6&67777 C C C"3s886BBBBC&{'=>>>>>>>>r61---!## S ;q;;;6RRRR -QQ777777777s*   G G.-G.4H 
H;H66H;rR   tuple[bytes, str]c                   |                      d          \  }}}d|vrt          d| d d                   |t          d          d                              dd          d	                                         pd
}t          |          dz  dz  t
          k    rt          d| d d                   	 t          j        |d          }n.# t          $ r!}t          d| | d d                   d }~ww xY w||fS )N,z;base64z data: URL must be base64-encoded@   r4   r5   ;   r   zapplication/octet-stream      zdata: URL exceeds size limitTvalidatezinvalid base64 in data: URL: )
	partitionr&   rD   splitr;   _MAX_INGEST_BYTESr"   base64	b64decoderK   )rR   header_payloaddeclaredr/   rW   s          r   r=   r=      s"   S))FAw;3B3HHHHc'llmm$**32215;;==[A[HGqQ!222;3B3HHHHL$777 L L L>>>AcrcFKKKKL>s   4C 
C6C11C6urlr)   c                    ddl m} ddlm}  ||           sdS  ||           }|r|                    d          pdS dS )u	  Return a human-readable block reason, or None when the URL is allowed.

    Pre-flight short-circuit: policy-blocked URLs are refused BEFORE any
    network I/O. ``_download_image`` re-checks policy internally (per attempt
    and against the final redirect target) — that second evaluation is
    intentional, not redundant: this one guarantees no bytes move for a
    blocked URL; the inner one covers redirects and non-resolver callers.
    Preserves the specific website-policy message so the agent sees *why*.
    r   )is_safe_url)check_website_accesszblocked: unsafe or private URLr   zblocked by website policyN)tools.url_safetyrn   tools.website_policyro   get)rl   rn   ro   blockeds       r   r?   r?      sv     -,,,,,999999;s 0//""3''G E{{9%%D)DD4r   r.   c                  K   dd l }ddlm} |                    dd          5 }t	          |j                  }d d d            n# 1 swxY w Y   	  || |           d {V  t          j        |j                   d {V 	 |	                    d           S # t          $ r$}t          t          |          | d	          d }~ww xY w# |	                    d           w xY w)
Nr   )_download_imagez.imgF)suffixdeleteT)
missing_okr6   r
   )tempfiletools.vision_toolsru   NamedTemporaryFiler   namerM   rN   rO   unlinkPermissionErrorr    r   )rl   ry   ru   tftmprW   s         r   r@   r@      sY     OOO222222		$	$F5	$	A	A R27mm              $oc3'''''''''&s~666666666 	

d
####  = = =3s88V<<<<= 	

d
####s5   AA	A	0B 
C#CCC
 
C"boolc                 x    t          j        dd                                                                          dv S )zTrue when the terminal backend runs directly on the host.

    Mirrors ``tools.browser_tool._is_local_backend`` and terminal_tool's own
    dispatch, which key off ``TERMINAL_ENV``.
    TERMINAL_ENVlocal)r   r	   )rE   getenvr;   rC   r   r   r   rP   rP      s3     9^W--3355;;==NNr   listc                 V    ddl m}   |             }|dz  |dz  |dz  |dz  |dz  |dz  gS )	a-  Agent-managed media cache directories under HERMES_HOME (host side).

    The only host paths vision may read under a non-local backend: gateway-
    downloaded inbound media and the tools' own URL-download temp dirs. Covers
    the consolidated ``cache/`` layout and the legacy flat directories.
    r   )get_hermes_homecacheimage_cacheaudio_cachevideo_cachetemp_vision_imagestemp_video_files)hermes_constantsr   )r   homes     r   _media_cache_rootsr      s^     100000?Dw}}}##!! r   rU   r   Optional[Path]c                   t                      r'	 |                                 S # t          $ r | cY S w xY wddlm} t           |t          |                               }	 |                                }n# t          $ r Y dS w xY wt                      D ]=}	 |                    |                                           |c S # t          $ r Y :w xY wdS )a  Return the host path to read, or ``None`` if a host read is not permitted.

    - Local backend: any path is permitted (chosen posture). Returns ``p``.
    - Non-local backend: permitted only if the path resolves inside a media
      cache root. A container-visible cache path (e.g. ``/root/.hermes/cache/
      images/x.png``) is first translated back to its host mount; anything that
      is not under a cache returns ``None`` so the caller routes it to the
      in-sandbox exec-read instead of reading the host filesystem.
    r   )from_agent_visible_cache_pathN)
rP   resolverK   tools.credential_filesr   r   r   r   relative_torL   )rU   r1   r   host_candidaterealroots         r   rH   rH      s    "## 	99;; 	 	 	HHH	 EDDDDD77A??@@N%%''   tt"$$  	T\\^^,,,KKK 	 	 	H	4s0   $ 33"A7 7
BB(C
CCr*   c                P    | sd S 	 ddl m}  ||           S # t          $ r Y d S w xY w)Nr   )get_active_env)tools.terminal_toolr   rK   )r*   r   s     r   _get_active_envr     sX     t666666~g&&&   tts    
%%c                  K   ddl }ddl}t          |j                  }|t	          d|  d|d          |                    t          |                     } |j        |j        dt          dz    d	| d
           d{V }|
                    dd          dk    rt	          d|  d|d          	 t          j        |
                    dd          d          }n)# t          $ r}	t          d|  d|	 |          d}	~	ww xY wt          |          t          k    rt!          d|d          t#          |dd|          S )a  Read the image bytes inside the sandbox (fail-closed when none exists).

    Reached when a host read is not permitted or the host file is absent. The
    agent can already ``cat`` any container file (file_operations.py reads
    root-owned mode-600 files this way), so this stays within the same sandbox
    boundary and never touches the host filesystem. ``--`` stops a leading-dash
    path from being parsed as a ``base64`` option; ``base64 -w0`` is GNU-only,
    so pipe through ``tr -d`` for BusyBox.

    Fail-closed: if there is no active sandbox env we refuse rather than falling
    back to a host read, so a non-cache host path under a sandbox never leaks.
    r   Nr9   z[' is not reachable inside the sandbox and no active sandbox session is available to read it	containerr
   zhead -c r^   z < z | base64 | tr -d '\n'
returncodezcould not read 'z' inside the sandboxoutputr	   Tra   z%sandbox returned non-image data for 'z': r4   image exceeds size limit)rM   shlexr   r*   r$   quoter   rN   executere   rr   rf   rg   rK   r&   rD   r"   r>   )
rU   r1   r   rM   r   envqpresr/   rW   s
             r   rQ   rQ     s      NNNLLL
#+
&
&C
{/ / / /K) ) ) 	) 
SVV		B!!H$q(HHRHHHJ J J J J J J JC ww|Q1$$GGGGSYdeeeeW" 5 5EEE W W WLLLsLLRUVVVVW
4yy$$$7SUUUUT2{C000s   5*C   
D*DDr/   declared_mimer   c                $   ddl m} t          |           t          k    rt	          d||           ||           }|Bd| dd                                         v rt          | d|	          S t          d
||          t          | ||	          S )u(  Intrinsic-correctness chokepoint: ingest byte cap + magic-byte sniff.

    The cap here is the generous 50MB *ingest* budget, not the 20MB provider
    payload cap — a 20-50MB image must survive this step so the call site can
    resize it under the payload cap. See ``_MAX_INGEST_BYTES``.
    r   )"_detect_image_mime_type_from_bytesr   r
   Ns   <svgi   zimage/svg+xml)r/   r0   r   z source is not a recognized image)rz   r   rD   re   r"   rC   r-   r&   )r/   r   r   r   r   sniffeds         r   r>   r>   ?  s     FEEEEE
4yy$$$7SPPPP0066Gd5D5k'')))) !dPPPP;VTTTTd@@@@r   )r   r   r1   r(   r2   r-   )rR   r   r2   rY   )rl   r   r2   r)   )rl   r   r2   r.   )r2   r   )r2   r   )rU   r   r1   r(   r2   r   )r*   r)   )rU   r   r1   r(   r   r   r2   r-   )
r/   r.   r   r   r   r   r   r   r2   r-   )#__doc__
__future__r   rM   rf   rE   redataclassesr   pathlibr   typingr   re   rK   r   r   r    r"   r$   r&   r(   r-   compilerA   rX   r=   r?   r@   rP   r   rH   r   rQ   r>   r   r   r   <module>r      s   4 # " " " " "   				 				 ! ! ! ! ! !             % , , , , ,9 , , ,	 	 	 	 	, 	 	 		 	 	 	 	' 	 	 		 	 	 	 	) 	 	 		 	 	 	 	) 	 	 		 	 	 	 	% 	 	 	 " " " " " " " "         RZ788
88 88 88 88v      *$ $ $ $"O O O O   (   @   +1 +1 +1 +1\A A A A A Ar   