
    `gjK                    l   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Zddlm	Z	 ddl
mZmZmZmZ ddlmZ  ej"                  e      ZdZdZeefZ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Zd'dZ d(dZ! G d d      Z"ddd	 	 	 d)dZ#d(dZ$d*dZ%ddddd	 	 	 	 	 	 	 d+dZ&d,dZ'd-dZ(y).u}  Write-approval gate + pending store for memory and skill writes.

Background
----------
The agent writes to two persistent stores that survive across sessions:

  * **memory** — MEMORY.md / USER.md, small (~200 char) declarative entries
  * **skills** — SKILL.md + supporting files, potentially huge (10-100 KB)

Both stores are written from two origins:

  * **foreground** — a normal agent turn (user is present / chatting)
  * **background_review** — the self-improvement review fork that runs after a
    turn and autonomously decides what to save (the source of the
    "wrong assumptions" users complained about)

This module lets the user gate those writes per-subsystem with a boolean
``write_approval``:

  * ``false`` (default) — write freely (the pre-gate behaviour)
  * ``true``            — require approval: do not commit the write; either
    prompt inline (memory, interactive CLI only) or **stage** it to a pending
    store and surface it for the user to approve or reject out-of-band

The size asymmetry between memory and skills is real and unavoidable: a memory
entry can be reviewed inline in a chat bubble; a 100 KB SKILL.md cannot. So
the gate stages BOTH to disk, but review affordances differ by subsystem
(see ``hermes_cli`` slash handlers): memory shows full content, skills show
metadata + a one-line gist + a ``diff`` escape hatch (CLI/dashboard/file).

Staging is mandatory for background-origin writes (a daemon thread cannot
block on an interactive prompt) and for gateway sessions (no inline prompt
channel — review happens via ``/memory pending``). Foreground CLI memory
writes prompt inline via the dangerous-command approval callback; skill
writes always stage (too big to eyeball mid-loop).

Pending records live under ``<HERMES_HOME>/pending/{memory,skills}/<id>.json``
so they survive process restarts and can be reviewed from CLI, gateway, or the
web dashboard.
    )annotationsN)Path)AnyDictListOptionalget_hermes_homememoryskillswrite_approvalc                    | t         vry	 ddlm}m}  |       } ||| t        d      }t        |      S # t
        $ r Y yw xY w)u'  Return whether the approval gate is enabled for ``subsystem``.

    Reads ``<subsystem>.write_approval`` from config.yaml. Defaults to
    ``False`` (gate off — writes flow freely) for any unset / invalid value so
    existing installs keep their current behaviour until the user opts in.
    Fr   )load_configcfg_get)default)_SUBSYSTEMShermes_cli.configr   r   
CONFIG_KEY	Exception_normalize_enabled)	subsystemr   r   cfgraws        G/root/.hermes/venv/lib/python3.12/site-packages/tools/write_approval.pywrite_approval_enabledr   J   sO     #:mc9j%@ c""  s   5 	A Ac                    t        | t              r| S t        | t              r | j                         j	                         dv S y)a  Coerce a config value to a bool. Default (unknown) is False (gate off).

    Accepts real bools and the usual truthy/falsey strings. YAML 1.1 parses
    bare ``on``/``off``/``yes``/``no`` as bools already, so the string branch
    is mostly for hand-edited configs.
    >   1onyestrueapproveenabledF)
isinstanceboolstrstriplower)values    r   r   r   \   s;     %%{{}""$(XXX    c                "    t               dz  | z  S )Npendingr	   )r   s    r   _pending_dirr,   n   s    y(944r)   c                  t        j                         j                  dd }|| |j                  dd      |xs dj	                         |xs dt        j
                         |d}	 t        |       }|j                  dd       || d	z  }|j                  d
      }|j                  t        j                  |dd      d       t        j                  ||       |S # t        $ r$}	t        j!                  d| |	d       Y d}	~	|S d}	~	ww xY w)uO  Persist a pending write and return a short record describing it.

    Args:
        subsystem: ``memory`` or ``skills``.
        payload: the exact kwargs needed to replay the write when approved
            (e.g. ``{"action": "add", "target": "user", "content": "..."}``
            for memory, or the full ``skill_manage`` kwargs for skills).
        summary: a one-line human-readable description shown in pending lists.
            For skills this is the LLM/heuristic gist; for memory it can be the
            entry text itself.
        origin: ``foreground`` or ``background_review`` — recorded for audit.

    Returns a dict with ``id`` and metadata. Best-effort: on disk failure it
    logs and still returns a record (the write is simply lost, which is the
    safe failure for an approval gate — nothing is silently committed).
    N   action 
foreground)idr   r/   summaryorigin
created_atpayloadT)parentsexist_ok.jsonz	.json.tmpF   )ensure_asciiindentutf-8encodingz$Failed to stage pending %s write: %s)exc_info)uuiduuid4hexgetr&   timer,   mkdirwith_suffix
write_textjsondumpsosreplacer   loggererror)
r   r6   r3   r4   pidrecorddpathtmpes
             r   stage_writerU   r   s    $ **,

2A
C++h+Mr((*(LiikFZ#	t,cU%= {+tzz&uQGRYZ


3 M  Z;YTXYYMZs   %A6C 	D
&DD
c                J   t        |       }|j                         sg S g }|j                  d      D ]7  }	 |j                  t	        j
                  |j                  d                   9 |j                  d        |S # t        $ r t        j                  d|       Y pw xY w)z;Return all pending records for ``subsystem``, oldest first.*.jsonr=   r>   z&Skipping unreadable pending record: %sc                &    | j                  dd      S )Nr5   r   )rD   )rs    r   <lambda>zlist_pending.<locals>.<lambda>   s    quu\15 r)   )key)r,   existsglobappendrI   loads	read_textr   rM   warningsort)r   rQ   recordsps       r   list_pendingre      s    YA88:	$&GVVH H	HNN4::akk7k&CDEH
 LL5L6N  	HNNCQG	Hs   4B  B"!B"c                    t        |       | dz  }|j                         sy	 t        j                  |j	                  d            S # t
        $ r Y yw xY w)z.Return a single pending record by id, or None.r9   Nr=   r>   )r,   r\   rI   r_   r`   r   )r   
pending_idrR   s      r   get_pendingrh      sU    	"
|5%99D;;=zz$..'.:;; s   $A	 		AAc                    t        |       | dz  }	 |j                         r|j                          y	 y# t        $ r"}t        j                  d| ||       Y d}~yd}~ww xY w)z4Delete a pending record. Returns True if it existed.r9   Tz#Failed to discard pending %s/%s: %sNF)r,   r\   unlinkr   rM   rN   )r   rg   rR   rT   s       r   discard_pendingrk      sk    	"
|5%99DV;;=KKM 
   V:IzSTUUVs    6 	A!AA!c                    t        |       }|j                         sy	 t        d |j                  d      D              S # t        $ r Y yw xY w)z9Cheap count of pending records (for notification badges).r   c              3      K   | ]  }d   yw)   N ).0_s     r   	<genexpr>z pending_count.<locals>.<genexpr>   s     /1/s   rW   )r,   r\   sumr]   r   )r   rQ   s     r   pending_countrt      sI    YA88:/affX./// s    ? 	A
Ac                 <    	 ddl m}   |        S # t        $ r Y yw xY w)a=  Return the active write origin: ``foreground`` or ``background_review``.

    Reuses the skill-provenance ContextVar, which the background review fork
    already sets (see ``agent.background_review`` /
    ``AIAgent._spawn_background_review``). Foreground agent turns leave it at
    the default ``foreground``.
    r   get_current_write_originr1   )tools.skill_provenancerw   r   rv   s    r   current_originry      s%    C')) s    	c                     t               dk(  S )Nbackground_review)ry   ro   r)   r   is_backgroundr|      s    222r)   c                  &    e Zd ZdZdZddddddZy)GateDecisionu  Result of evaluating the write gate for a single write attempt.

    Exactly one of the boolean flags is True:
      * ``allow``  — proceed with the real write (gate off, or an inline
        approval was granted).
      * ``blocked`` — refuse the write (the user denied an inline approval
        prompt). ``message`` explains why; surface it to the agent.
      * ``stage``  — do not write; the caller should stage the payload via
        ``stage_write`` (gate on, and no inline prompt is available — gateway,
        background review, script, or any skill write). ``message`` is the
        user-facing "staged for approval" note.
    allowblockedstagemessageFr0   c               <    || _         || _        || _        || _        y )Nr   )selfr   r   r   r   s        r   __init__zGateDecision.__init__   s    

r)   N)__name__
__module____qualname____doc__	__slots__r   ro   r)   r   r~   r~      s     9I %uE2 r)   r~   r0   )inline_summaryinline_detailc               $   t        |       st        d      S t               }| t        k(  s|r!| t        k(  rdnd}t        dd|  d| d      S t	               r-t        ||      }|du rt        d      S |d	u rt        dd
      S t        dd      S )u  Decide what to do with a pending write for ``subsystem``.

    Args:
        subsystem: ``memory`` or ``skills``.
        inline_summary: short description used as the inline approval prompt
            header (memory foreground path only).
        inline_detail: full content shown in the inline prompt (memory entries
            are small; skills never take the inline path).

    Decision matrix:
        gate off (default)                    → allow (writes flow freely)
        gate on, memory + interactive CLI     → inline approve/deny prompt
        gate on, memory + gateway/script/bg   → stage
        gate on, skills (any origin)          → stage (too big to review inline)

    Note: there is no config-driven "blocked" outcome — the gate only ever
    delays a write for approval, never silently refuses it. ``blocked`` is
    still produced when the user *actively denies* an inline prompt.
    T)r   z/skills pendingz/memory pendingzStaged for approval (u6   .write_approval is on). Not yet saved — review with .)r   r   Fz6Memory write denied by user. The change was not saved.)r   r   ua   Staged for approval (memory.write_approval is on). Not yet saved — review with /memory pending.)r   r~   r|   SKILLS_interactive_approval_available_prompt_inline_memory_approval)r   r   r   
backgroundwheregranteds         r   evaluate_gater      s    * "),$''J Fj%.&%8!>O'	{ 3116q:
 	
 '(0Od?d++eP  = r)   c                 @    	 ddl m}   |        duS # t        $ r Y yw xY w)u  True when a foreground memory write can be approved inline.

    Inline prompting requires a per-thread approval callback registered by the
    interactive CLI (``tools.terminal_tool.set_approval_callback``). Every
    other surface stages instead:

    * **Gateway/API sessions** — the dangerous-command ``/approve`` round-trip
      lives in the pending-approval queue (``submit_pending`` +
      ``_await_gateway_decision``), which ``prompt_dangerous_approval`` never
      reaches; trying to prompt from a gateway session would hit the
      ``input()`` fallback and silently deny. Staging gives the user a real
      review affordance (``/memory pending``) instead.
    * Scripts, cron, and background threads — no user present.
    r   _get_approval_callbackNF)tools.terminal_toolr   r   r   s    r   r   r   ;  s*    >%'t33 s    	c                (   	 ddl m}  |       }|y| j                         xs d}|j                         }d| }|r|n|}	  |||d      }|d	v ry
|dk(  ryy# t        $ r Y yw xY w# t        $ r }	t        j                  d|	       Y d}	~	yd}	~	ww xY w)uo  Prompt the user inline to approve a memory write.

    Returns True (approved), False (denied), or None (no interactive prompt
    available / prompt failed → caller should stage instead).

    Reuses the per-thread CLI approval callback registered for dangerous
    commands (``tools.terminal_tool.set_approval_callback``). The callback is
    invoked directly — NOT via ``prompt_dangerous_approval`` — because that
    wrapper falls back to ``input()`` (deadlock-prone under prompt_toolkit,
    see #15216) and converts callback errors into a silent deny; here a
    failed prompt must stage the write instead.
    r   r   NzSave to memory?zSave to memory: F)allow_permanentz(Inline memory approval prompt failed: %s>   oncesessionTdeny)r   r   r   r&   rM   rN   )
r3   detailr   callbackheaderbodydescriptioncommandchoicerT   s
             r   r   r   Q  s    > &'H ]]_1 1F<<>D$VH-KdG
';F
 $$ ;  &  ?Cs)   A A( 	A%$A%(	B1BB)content	file_path
old_string
new_stringc          	        | dv ra|r_t        |      }t        |      dk\  rt        |      dz  dz    dnt        |       d}| dk(  rdnd}|r| d| d	| d
| dS | d| d| dS | dk(  rE|xs d}	|r|j                  d      dz   nd}
|r|j                  d      dz   nd}d| d|	 d| d|
 d	S | dk(  r	d| d| dS | dk(  r	d| d| dS | dk(  rd| dS |  d| dS )ug  Build a one-line human gist for a pending skill write.

    Heuristic, no model call — the gist surfaces enough to decide approve/reject
    in a chat bubble, while the full diff stays behind /skills diff (CLI/
    dashboard/file). For create/edit it pulls the frontmatter ``description:``;
    for patch/write_file it describes the size of the change.
    >   editcreatei   rn   z KBz charsr   rewritez 'u   ' — z ()z' (patchSKILL.md
r   zpatch 'z' z (+z/-z lines)
write_filezwrite z in ''remove_filezremove z from 'deletedelete skill ')_frontmatter_descriptionlencount)r/   namer   r   r   r   descsizeverbtargetremovedaddeds               r   
skill_gistr     sb    ##'036w<43G#g,$&*+3/PST[P\~]cMd!X-x9V2dV6$r$q99r$s4&**(j0:*""4(1,.8
  &*abE7"WIWEE	{%vQ//74&22vQ''XRvQr)   c                    ddl }|j                  d| |j                        }|sy|j                  d      j	                         j	                  d      }|dd S )zBExtract the ``description:`` value from SKILL.md YAML frontmatter.r   Nz^description:\s*(.+)$r0   rn   z'"   )research	MULTILINEgroupr&   )r   r   mr   s       r   r   r     sO    
		*GR\\BA771:##E*D:r)   c                   ddl }| j                  di       }|j                  dd      }|j                  dd      }|dk(  r|j                  d      xs dS 	 dd	lm} d}d
}|c ||      }|rY|d   }	|dk(  r|	d
z  }
n&|dv r|j                  d      xs d
}|	|z  }
|}n|	d
z  }
	 |
j                         r|
j                  d      }|dk(  r|j                  d      xs d}n|dk(  rH|j                  d      xs d}|j                  d      xs d}|r|j                  ||      nd|d|d}nL|dk(  r|j                  d      xs d}n1|dk(  rd|j                  d       d| dS |dk(  rd| dS d| d | d!S |j                  |j                  d"#      |j                  d"#      d$| d%| &      }dj                  |      }|xs d'S # t        $ r d}Y }w xY w# t        $ r d}Y &w xY w)(aF  Build a full unified diff (or full content) for a staged skill write.

    Used by /skills diff <id> on a surface that can render it (CLI pager, web
    dashboard, or by opening the pending JSON file). For create this is the new
    file content; for edit/patch it is a unified diff against the current
    on-disk skill.
    r   Nr6   r/   r0   r   r   r   )_find_skillr   rR   r   >   r   r   r   r=   r>   r   r   r   z(patch u    → r   r   file_contentr   zremove file: z from skill 'r   r   r   (z on 'z')T)keependsza/zb/)fromfiletofilez(no textual change))difflibrD   tools.skill_manager_toolr   r   r\   r`   rL   unified_diff
splitlinesjoin)rP   r   r6   r/   r   r   currenttarget_labelfoundbaserd   relnewold_snew_sdifftexts                    r   skill_pending_diffr     sU    jjB'G[[2&F;;vr"DI&,"-8 GLD!=D:%22kk+.<*3J":%88:kk7k;G kk)$*	7	L)/RL)/R/6gooeU+geYeTYS\\]<^	<	kk.)/R	=	 w{{;78dV1MM	8	vQ''6(%vR((D)%l^$L>"	   D 774=D((([  (  s$   G #"G GGG,+G,)r   r%   returnr$   )r(   r   r   r$   )r   r%   r   r   )
r   r%   r6   Dict[str, Any]r3   r%   r4   r%   r   r   )r   r%   r   zList[Dict[str, Any]])r   r%   rg   r%   r   zOptional[Dict[str, Any]])r   r%   rg   r%   r   r$   )r   r%   r   int)r   r%   )r   r$   )r   r%   r   r%   r   r%   r   r~   )r3   r%   r   r%   r   zOptional[bool])r/   r%   r   r%   r   r%   r   r%   r   r%   r   r%   r   r%   )r   r%   r   r%   )rP   r   r   r%   ))r   
__future__r   rI   loggingrK   rE   rA   pathlibr   typingr   r   r   r   hermes_constantsr
   	getLoggerr   rM   MEMORYr   r   r   r   r   r,   rU   re   rh   rk   rt   ry   r|   r~   r   r   r   r   r   r   ro   r)   r   <module>r      s  'R #   	    , , ,			8	$ 
	v 
#$$5%%),%1?%P	3 . <>');!$;.:;|,,f :< "b!#  03  (+ >@)r)   