
    sj                     V   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mZ ddlm	Z
  ej        e          Z eh d          Z eh d          Z ej        dd                                          d	v Zg d
ZdZ ej        de d          Z ej        dej                  ZdZdZ ej        eej                  Z ej        d          Z ej        de de de ej                  Z ej        de de ej        ej        z            Z dZ! ej        de! dej        ej        z            Z" ej        dej                  Z#de$de%de&fdZ'dd de$d!e%d"e&de&fd#Z(d$e$de&fd%Z)d&Z* ej        d'e* d(ej                  Z+ ej        d)ej                  Z,d*Z- ej        d+e- d,ej                  Z. ej        d-          Z/ ej        d.          Z0 ej        d/ej                  Z1 ej        d0ej                  Z2 ej        d1          Z3 ej        d2          Z4 ej        d3          Z5 ej        d4          Z6 ej        d5          Z7 ej        d6          Z8 ej        d7ej                  Z9 ej        d8          Z: ej        d9          Z; ed:          Z< ej        d;d<=                    e          z   d=z             a>d>e$de$fd?Z? ej        d@          Z@dAdAdBdCdDdEdFe$dGe%dHe%dIe%dJe$dKe$de$fdLZAdMe$de$fdNZBdOe$de$fdPZCd>e$de$fdQZDd>e$de$fdRZEdSe$de$fdTZFd>e$de$fdUZGdFeHde$fdVZId>e$de$fdWZJd>e$de$fdXZKdMe$de$fdYZLdZdZdZdZd[d>e$d\e&d]e&d^e&d_e&de$fd`ZM eh da          ZN eh db          ZOdce$dz  de&fddZPdce$dz  de&fdeZQ	 dzdZdfdge$dce$dz  d\e&de$fdhZRdie$de$fdjZSdie$de&fdkZTdie$de&fdlZU eVdm eD                       aWd>e$de&fdnZXi ZYeZe[do<    ej\                    Z]de^fdpZ_d{dqZ`d|dse$de%fdtZad{duZbdvZcd>e$de&fdwZd G dx dyeje                  ZfdS )}a2  Regex-based secret redaction for logs and tool output.

Applies pattern matching to mask API keys, tokens, and credentials
before they reach log files, verbose output, or gateway logs.

Short tokens (< 18 chars) are fully masked. Longer tokens preserve
the first 6 and last 4 characters for debuggability.
    N)unquote_plus)_BLOCKED_PROJECT_ENV_BASENAMES>   x-amz-signaturejwtkeyauthcodetokenapikeysecretapi_keysessionid_tokenpassword	signatureaccess_tokenclient_secretrefresh_token>   r   r   r   r
   r   r   r   r   r   private_keyr   authorizationr   r   HERMES_REDACT_SECRETStrue>   1onyesr   )8zsk-[A-Za-z0-9_-]{10,}zghp_[A-Za-z0-9]{10,}zgithub_pat_[A-Za-z0-9_]{10,}zgho_[A-Za-z0-9]{10,}zghu_[A-Za-z0-9]{10,}zghs_[A-Za-z0-9]{10,}zghr_[A-Za-z0-9]{10,}zxapp-\d+-[A-Za-z0-9-]{10,}zxox[baprs]-[A-Za-z0-9-]{10,}zAIza[A-Za-z0-9_-]{30,}zpplx-[A-Za-z0-9]{10,}zfal_[A-Za-z0-9_-]{10,}zfc-[A-Za-z0-9]{10,}zbb_live_[A-Za-z0-9_-]{10,}zgAAAA[A-Za-z0-9_=-]{20,}zAKIA[A-Z0-9]{16}zsk_live_[A-Za-z0-9]{10,}zsk_test_[A-Za-z0-9]{10,}zrk_live_[A-Za-z0-9]{10,}zSG\.[A-Za-z0-9_-]{10,}zhf_[A-Za-z0-9]{10,}zr8_[A-Za-z0-9]{10,}znpm_[A-Za-z0-9]{10,}zpypi-[A-Za-z0-9_-]{10,}zdop_v1_[A-Za-z0-9]{10,}zdoo_v1_[A-Za-z0-9]{10,}zam_[A-Za-z0-9_-]{10,}zsk_[A-Za-z0-9_]{10,}ztvly-[A-Za-z0-9]{10,}zexa_[A-Za-z0-9]{10,}zgsk_[A-Za-z0-9]{10,}zsyt_[A-Za-z0-9]{10,}zretaindb_[A-Za-z0-9]{10,}zhsk-[A-Za-z0-9]{10,}zmem0_[A-Za-z0-9]{10,}zbrv_[A-Za-z0-9]{10,}zxai-[A-Za-z0-9]{30,}zntn_[A-Za-z0-9]{10,}zfw-[A-Za-z0-9]{30,}zfw_[A-Za-z0-9]{30,}zfpk_[A-Za-z0-9]{30,}zglpat-[A-Za-z0-9_\-]{10,}zgloas-[A-Za-z0-9_\-]{10,}zgldt-[A-Za-z0-9_\-]{10,}zglrt-[A-Za-z0-9_.\-]{10,}zglrtr-[A-Za-z0-9_.\-]{10,}zglcbt-[A-Za-z0-9_\-]{10,}zglptt-[A-Za-z0-9_\-]{10,}zglft-[A-Za-z0-9_\-]{10,}zglimt-[A-Za-z0-9_\-]{10,}zglagent-[A-Za-z0-9_\-]{10,}zglsoat-[A-Za-z0-9_\-]{10,}zglffct-[A-Za-z0-9_\-]{10,}zglwt-[A-Za-z0-9_\-]{10,}zGR1348941[A-Za-z0-9_\-]{10,}zpk-lf-[A-Za-z0-9\-]{8,}zE(?:API_?KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|PASS|PW|CREDENTIAL|AUTH)z([A-Z0-9_]{0,50}z&[A-Z0-9_]{0,50})\s*=\s*(['\"]?)(\S+)\2zu([a-z0-9_]+(?:_|^)(?:key|pass|pw|token|secret|password|passwd|credential|auth)(?=[^a-z0-9_]|$))\s*=\s*(['\"]?)(\S+)\2z?(?:api[ _.\-]?key|token|secret|passwd|password|credential|auth)z(['\"]?)([^\s&]+?)\2(?=[\s&]|$)z0^(?:os\.(?:getenv|environ)|process\.env|\$ENV\{)z#([A-Za-z0-9_\-]++\.[A-Za-z0-9_.\-]*z"[A-Za-z0-9_.\-]*+|[A-Za-z0-9_.\-]*z%[A-Za-z0-9_.\-]*\.[A-Za-z0-9_.\-]++)=z((^[ \t]*(?:export[ \t]+)?[A-Za-z0-9_\-]*z[A-Za-z0-9_\-]*)=z:(?:api[ _.\-]?key|token|secret|passwd|password|credential)z(^[ \t]*+[A-Za-z0-9_.\-]*z/[A-Za-z0-9_.\-]*+)(:[ \t]*+)(?!['\"])([^\s&]++)zy(?:api|auth|access|refresh|session|secret)[ _.\\-]?(?:key|token)|token|secret|passwd|password|pass|pw|credential|auth|keysireturnc                 v   |dk    rdS | |dz
           | |         }}|                                 sdS |                                r|                                rdS |                                rI|                                r5|dz   t          |           k     r| |dz                                            rdS dS )z=True if position ``i`` in ``s`` begins a word (not mid-word).r   T   F)isalphaisupperislowerlen)r   r   prevcurs       0/home/agent/.hermes/hermes-agent/agent/redact.py_is_word_startr(     s    Avvt!a%!A$#D<<>> t
{{}}  t {{}}  AECFFNNqQx?O?O?Q?QNt5    Tallow_pluraljr+   c                   |t          |           k    rdS | |         }|                                sdS |                                r| |dz
                                           rdS |r|dv rt	          | |dz   d          S dS )z<True if position ``j`` (exclusive end) in ``s`` ends a word.Tr    sSFr*   )r$   r!   r"   r#   _is_word_end)r   r,   r+   r&   s       r'   r/   r/     s    CFF{{t
A$C;;== t
{{}} 1q5))++ t :tAq1u599995r)   r   c                    d | D             }|rt          d |D                       rft                              |           D ]I}t          | |                                          r%t          | |                                          r dS JdS t                              |           D ]I}t          | |                                          r%t          | |                                          r dS JdS )u|  True if ``key`` contains a secret keyword at a word boundary.

    Post-match validator for _CFG_DOTTED_RE / _CFG_ANCHORED_RE /
    _YAML_ASSIGN_RE hits — rejects prose words that merely embed a keyword
    (``secretary``, ``tokenizer``, ``authored``). Safe to call with the
    _ENV_ASSIGN_RE key too: all-caps keys short-circuit to the legacy
    embedded-match behavior.
    c                 :    g | ]}|                                 |S  )r!   .0cs     r'   
<listcomp>z+_key_has_secret_keyword.<locals>.<listcomp>.  s%    ---Q-q---r)   c              3   >   K   | ]}|                                 V  d S N)r"   r3   s     r'   	<genexpr>z*_key_has_secret_keyword.<locals>.<genexpr>/  s*      44qqyy{{444444r)   TF)all_KEY_KEYWORD_REfinditerr(   startr/   end)r   lettersms      r'   _key_has_secret_keywordrA   %  s     .-#---G 	344G44444 	 !))#.. 	 	Ac17799-- ,sAEEGG2L2L ttu%%c**  #qwwyy)) 	l3.H.H 	445r)   z(?:api_?[Kk]ey|token|secret|password|access_token|refresh_token|auth_token|bearer|secret_value|raw_secret|secret_input|key_material)z("z")\s*:\s*"([^"]+)"z?((?:Proxy-)?Authorization:\s*)([A-Za-z][\w.+-]*\s+)?([^\s\"']+)zS(?:x-api-key|x-goog-api-key|api-key|apikey|x-api-token|x-auth-token|x-access-token)(z\s*:\s*)(\S+)z#(bot)?(\d{8,}):([-A-Za-z0-9_]{30,})zH-----BEGIN[A-Z ]*PRIVATE KEY-----[\s\S]*?-----END[A-Z ]*PRIVATE KEY-----zO((?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp)://[^:\s]+:)([^@\s]+)(@)zA((?:https?|wss?|git|ssh|ftp|ftps|sftp)://)([^\s:@/]{8,})(@[^\s]+)z2eyJ[A-Za-z0-9_-]{10,}(?:\.[A-Za-z0-9_=-]{4,}){0,2}z (\+[1-9]\d{6,14})(?![A-Za-z0-9])z;(https?|wss?|ftp)://([^\s/?#]+)([^\s?#]*)\?([^\s#]+)(#\S*)?z+(https?|wss?|ftp)://([^/\s:@]+):([^/\s@]+)@z.([?#&;])([A-Za-z0-9_.~+%\-]+)=([^#&;\s\"'<>]*)z(//)([^/\s?#@]+)@z`\b((?:GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|TRACE|CONNECT)\s+[^ \t\r\n\"']*?)\?([^ \t\r\n\"']+)zH^[A-Za-z_][A-Za-z0-9_.-]*=[^&\s]*(?:&[A-Za-z_][A-Za-z0-9_.-]*=[^&\s]*)+$z5[\x00-\x1f\x7f\u200b-\u200f\u2028-\u202f\u2060\ufeff]zAABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-.(?<![A-Za-z0-9_-])(|)(?![A-Za-z0-9_-])textc                    t                               d|           }|| k    r| S d t          |           D             }t          |           }g }t                              |          D ]}|                    d          }||                    d                   }||                    d          dz
           dz   }	| ||	         }
d|
v sd|
v rt          	                    |
          rt          d |
D                       r@|	t          |           k    s| |	         dk    r!|                    ||	 ||          f           t          |          D ]\  }}	}t          |          |||	<   d                    |          S )u  Mask tokens whose body is split by control/zero-width characters.

    A credential like ``sk-abc\x1bdef456…`` or ``ghp_abc\n123def…`` has its
    token body interrupted, so the contiguous _PREFIX_RE cannot match it and
    the secret leaks verbatim (issue #77484). Strategy: build a copy with all
    control chars removed (the token is contiguous again, matching even when
    each fragment alone is too short), match on that, then mask the
    corresponding span in the *original* — but only when the original span
    contains solely token-body and control chars (a match that crosses into a
    different line's unrelated text, e.g. ``EXA_API_KEY=*** is rejected).
     c                 L    g | ]!\  }}t                               |          |"S r2   )_CONTROL_CHARS_REmatch)r4   r   r5   s      r'   r6   z._mask_control_split_tokens.<locals>.<listcomp>  s0    PPPda5F5L5LQ5O5OPPPPr)   r    
c              3   \   K   | ]'}|t           v pt                              |          V  (d S r8   )_TOKEN_BODY_CHARSrJ   rK   r3   s     r'   r9   z-_mask_control_split_tokens.<locals>.<genexpr>  sP         &&D*;*A*A!*D*D      r)   =)rJ   sub	enumeratelist
_PREFIX_REr<   groupr=   r>   searchr:   r$   appendreversedjoin)rF   mask_fnstrippedorig_idxoutmatchesr@   body
start_origend_origspanreplacements               r'   _mask_control_split_tokensrd     s    !$$R..H4PPiooPPPH
t**CG  ** B Bwwqzzaggajj)
AEE!HHqL)A- Jx'(DLLDDLLj.?.?.E.EL        	BT**d8n.C.CNNJ''$--@AAA-5g->-> 5 5)
Hk#'#4#4Jx  773<<r)   z?[\x00-\x1f\x7f\x80-\x9f\u200b-\u200f\u202a-\u202e\u2060-\u2064]      ***rH   )headtailfloorplaceholderemptyvaluerh   ri   rj   rk   rl   c                    | s|S t                               d|           } | s|S t          |           |k     r|S | d|          d| | d          S )u  Mask a secret for display, preserving ``head`` and ``tail`` characters.

    Canonical helper for display-time redaction across Hermes — used by
    ``hermes config``, ``hermes status``, ``hermes dump``, and anywhere
    a secret needs to be shown truncated for debuggability while still
    keeping the bulk hidden.

    Args:
        value:       The secret to mask. ``None``/empty returns ``empty``.
        head:        Leading characters to preserve. Default 4.
        tail:        Trailing characters to preserve. Default 4.
        floor:       Values shorter than ``head + tail + floor_margin`` are
                     fully masked (returns ``placeholder``). Default 12 —
                     matches the existing config/status/dump convention.
        placeholder: Value returned for too-short inputs. Default ``"***"``.
        empty:       Value returned when ``value`` is falsy (None, ""). The
                     caller can override this to e.g. ``color("(not set)",
                     Colors.DIM)`` for user-facing display.

    Examples:
        >>> mask_secret("sk-proj-abcdef1234567890")
        'sk-p...7890'
        >>> mask_secret("short")                         # fully masked
        '***'
        >>> mask_secret("")                              # empty default
        ''
        >>> mask_secret("", empty="(not set)")           # empty override
        '(not set)'
        >>> mask_secret("long-token", head=6, tail=4, floor=18)
        '***'
    rH   Nz...)_DISPLAY_CONTROL_RErQ   r$   )rm   rh   ri   rj   rk   rl   s         r'   mask_secretrp   '  ss    P    ##B..E 
5zzEETEl..udUVV}...r)   r
   c                 0    | sdS t          | ddd          S )uO   Mask a log token — conservative 18-char floor, preserves 6 prefix / 4 suffix.rg      re      )rh   ri   rj   )rp   )r
   s    r'   _mask_tokenrt   \  s(      uu11B7777r)   queryc                 `   | s| S g }|                      d          D ]~}d|vr|                    |           |                    d          \  }}}|                                t          v r|                    | d           i|                    |           d                    |          S )a   Redact sensitive parameter values in a URL query string.

    Handles `k=v&k=v` format. Sensitive keys (case-insensitive) have values
    replaced with `***`. Non-sensitive keys pass through unchanged.
    Empty or malformed pairs are preserved as-is.
    &rP   =***)splitrW   	partitionlower_SENSITIVE_QUERY_PARAMSrY   )ru   partspairr   _rm   s         r'   _redact_query_stringr   d  s      EC    d??LLs++Q99;;111LLC&&&&LL88E??r)   c                 f    dt           j        dt          fd}t                              ||           S )u   Scan text for URLs with query strings and redact sensitive params.

    Catches opaque tokens that don't match vendor prefix regexes, e.g.
    `https://example.com/cb?code=ABC123&state=xyz` → `...?code=***&state=xyz`.
    r@   r   c                    |                      d          }|                      d          }|                      d          }t          |                      d                    }|                      d          pd}| d| | d| | S )	Nr          re      rH   ://?rU   r   )r@   scheme	authoritypathru   fragments         r'   _subz&_redact_url_query_params.<locals>._sub  s    GGAJJ	wwqzz$QWWQZZ00771::#@@Y@@@u@h@@@r)   )reMatchstr_URL_WITH_QUERY_RErQ   rF   r   s     r'   _redact_url_query_paramsr   z  sD    A AS A A A A !!$---r)   c                 :    t                               d |           S )zStrip `user:password@` from HTTP/WS/FTP URLs.

    DB protocols (postgres, mysql, mongodb, redis, amqp) are handled
    separately by `_DB_CONNSTR_RE`.
    c                 ^    |                      d           d|                      d           dS )Nr    r   r   :***@rU   r@   s    r'   <lambda>z&_redact_url_userinfo.<locals>.<lambda>  s+    QWWQZZ55AGGAJJ555 r)   )_URL_USERINFO_RErQ   rF   s    r'   _redact_url_userinfor     s%     55  r)   namec                     | }t          d          D ]}t          |          }||k    r n|}|                                                    dd          S )zCDecode a URL parameter name for bounded, case-insensitive matching.r   -r   )ranger   casefoldreplace)r   decodedr   
next_values       r'   _canonical_url_param_namer     sb    G1XX  !'**
  E%%c3///r)   c                     dt           j        dt          fd}dt           j        dt          fd}t                              ||           } t
                              ||           S )aK  Redact credentials from absolute, relative, and network URL references.

    This is intentionally stricter than display/log redaction and is used only
    at explicit secret-egress boundaries. It preserves original keys,
    separators, public parameters, hosts, and paths while masking sensitive
    values and URL userinfo.
    rK   r   c                     t          |                     d                    t          vr|                     d          S |                     d           |                     d           dS )Nr   r   r    rx   )r   rU   r|   )rK   s    r'   _redact_paramz5_redact_strict_url_credentials.<locals>._redact_param  sX    $U[[^^44<SSS;;q>>!++a..6%++a..6666r)   c                     |                      d          }d|v r3|                    d          \  }}}|                      d           | dS |                      d           dS )Nr   :r    r   z***@)rU   rz   )rK   userinfousernamer   	_passwords        r'   _redact_userinfoz8_redact_strict_url_credentials.<locals>._redact_userinfo  sk    ;;q>>(??%-%7%7%<%<"Hakk!nn5h5555++a..&&&&r)   )r   r   r   _STRICT_URL_PARAM_RErQ   _STRICT_URL_USERINFO_RE)rF   r   r   s      r'   _redact_strict_url_credentialsr     sx    7RX 7# 7 7 7 7
' 'S ' ' ' '  ##M488D"&&'7>>>r)   c                     t          | dnt          |                     }|s|S t          |          }t          |          }|S )a  Mask secrets in a CDP/browser endpoint URL before it is logged.

    The global ``redact_sensitive_text`` deliberately passes web-URL query
    params and ``user:pass@`` userinfo through unmasked (OAuth callbacks,
    magic-link / pre-signed URLs the agent is meant to follow -- see the
    web-URL note above). CDP discovery endpoints are NOT such a workflow:
    their query-string tokens and userinfo passwords are pure credentials
    that must never reach the logs. So for CDP URLs we opt INTO the two URL
    redactors that the global pass leaves off.

    This is the single source of truth for redacting a CDP URL that is passed
    *directly* to a log or error message. Callers that instead need to redact an
    exception whose text embeds the URL (e.g. a ``websockets`` connect error)
    should route that through their own error-text helper, which delegates here
    -- see ``tools.browser_supervisor._redact_cdp_error_text``.
    NrH   )redact_sensitive_textr   r   r   )rm   rF   s     r'   redact_cdp_urlr     sK    " !u}#e**EED #D))D%%DKr)   c                 f    dt           j        dt          fd}t                              ||           S )zARedact sensitive query params in HTTP access-log request targets.r@   r   c                 ~    |                      d          }t          |                      d                    }| d| S )Nr    r   r   r   )r@   prefixru   s      r'   r   z6_redact_http_request_target_query_params.<locals>._sub  s<    $QWWQZZ00""5"""r)   )r   r   r   _HTTP_REQUEST_TARGET_QUERY_RErQ   r   s     r'   (_redact_http_request_target_query_paramsr     s>    # #S # # # # ),,T4888r)   c                     | rd| v sd| vr| S t                               |                                           s| S t          |                                           S )uN  Redact sensitive values in a form-urlencoded body.

    Only applies when the entire input looks like a pure form body
    (k=v&k=v with no newlines, no other text). Single-line non-form
    text passes through unchanged. This is a conservative pass — the
    `_redact_url_query_params` function handles embedded query strings.
    rL   rw   )_FORM_BODY_RErK   stripr   r   s    r'   _redact_form_bodyr     sZ      44<<3d??tzz||,, 

---r)   c                 h    | sdS d}t           D ]}|                     |          r|} n|rd| dndS )u  Redact a prefix-matched credential to a NON-REUSABLE sentinel.

    Unlike :func:`_mask_token` (which keeps head/tail chars — fine for logs
    that are never fed back into a config), this emits a marker that:

    * cannot be mistaken for a usable-but-truncated key, so an agent that
      reads it from a config file and writes it back does NOT corrupt the
      stored credential into a dead 13-char string (issue #35519); and
    * still does not leak the secret material (no head/tail chars).

    The vendor prefix label is preserved for debuggability so the agent can
    still tell *which* credential is present (e.g. a GitHub PAT vs an OpenAI
    key) without seeing any of its bytes.
    u   «redacted-secret»rH   u   «redacted:u   …»)_PREFIX_SUBSTRINGS
startswith)r
   labelrQ   s      r'   _mask_token_nonreusabler     sl      %$$ E!  C   	EE	 */I%%%%%4IIr)   F)force	code_file	file_readredact_url_credentialsr   r   r   r   c                ~   | dS t          | t                    st          |           } | s| S |s	t          s| S |rd}t          |           r>|rt          nt
          t          |           } t                              fd|           } |sd| v rd }t                              ||           } d| vrt                              ||           } d| vrPt                              |           r6t                              ||           } t                              ||           } d| v r"d| v rd	 }t                              ||           } d| v r"d| vrd
 }t                               ||           } d| v sd| v rt"                              d |           } d| v rt$                              d |           } d| v rd }t&                              ||           } d| v rd| v rt(                              d|           } d| v rY|rd }	t*                              |	|           } nt*                              d |           } t,                              d |           } d| v rt.                              d |           } |rt1          |           } d| v rd| v rt3          |           } d| v rd }
t4                              |
|           } | S )u  Apply all redaction patterns to a block of text.

    Safe to call on any string -- non-matching text passes through unchanged.
    Enabled by default. Disable via security.redact_secrets: false in config.yaml.
    Set force=True for safety boundaries that must never return raw secrets
    regardless of the user's global logging redaction preference.

    Set redact_url_credentials=True at non-navigation egress boundaries to
    additionally redact credential-named query parameters and ``user:pass@``
    URL userinfo. The default remains False because actionable OAuth callback,
    magic-link, and pre-signed URLs must survive ordinary tool flows unchanged.

    Set code_file=True to skip the ENV-assignment and JSON-field regex
    patterns when the text is known to be source code (e.g. MAX_TOKENS=***
    constants, "apiKey": "test" fixtures). Prefix patterns, auth headers,
    private keys, DB connstrings, JWTs, and URL secrets are still redacted.

    Set file_read=True for file *content* returned to the agent (read_file /
    search_files / cat). Secrets are STILL redacted — they are never exposed —
    but prefix-matched credentials are replaced with a non-reusable sentinel
    (``«redacted:ghp_…»``) instead of a head/tail-preserving mask
    (``ghp_S1...Pn2T``). The old mask looked like a real-but-truncated key, so
    an agent reading it from config.yaml and writing it back silently corrupted
    the stored credential into a dead 13-char value → 401 (issue #35519). The
    sentinel is syntactically invalid as a token, so it can't be mistaken for a
    usable key or written back as one. Implies code_file=True (config/data
    files shouldn't trigger the source-code ENV/JSON false-positive paths).

    Performance: each regex pattern is gated behind a cheap substring
    pre-check (e.g. ``"=" in text`` for ENV assignments, ``"://" in text``
    for URLs, ``"eyJ" in text`` for JWTs). On a typical hermes log line
    (no secrets) this drops the 13-pattern scan from ~5.6us to ~1.8us per
    record (-68%). The pre-checks are conservative — false positives
    still run the full regex, which then doesn't match. False negatives
    are impossible because every regex requires the gated substring to
    match.
    NTc                 @     |                      d                    S )Nr    r   )r@   _prefix_subs    r'   r   z'redact_sensitive_text.<locals>.<lambda>K  s    AGGAJJ(?(? r)   rP   c                 V   |                      d          |                      d          |                      d          }}}t                              |          r|                      d          S t          |          s|                      d          S | d| t	          |           | S )Nr    r   r   r   rP   rU   _ENV_LOOKUP_VALUE_RErK   rA   rt   )r@   r   quoterm   s       r'   _redact_envz*redact_sensitive_text.<locals>._redact_envP  s    %&WWQZZQWWQZZUe (--e44 &771::% /t44 &771::%CCCE(:(:CECCCr)   r   r   "c                     |                      d          |                      d          }}t                              |          r|                      d          S | dt          |           dS )Nr    r   r   z: "r   )rU   r   rK   rt   )r@   r   rm   s      r'   _redact_jsonz+redact_sensitive_text.<locals>._redact_jsonz  sf    WWQZZU (--e44 &771::%77+e"4"47777r)   c                 P   |                      d          |                      d          |                      d          }}}t                              |          r|                      d          S t          |          s|                      d          S | | t	          |           S )Nr    r   r   r   r   )r@   r   seprm   s       r'   _redact_yamlz+redact_sensitive_text.<locals>._redact_yaml  s    "#''!**aggajj!''!**%S (--e44 &771::% /s33 &771::%8s8K$6$6888r)   uthorizationUTHORIZATIONc                     |                      d          |                      d          pdz   t          |                      d                    z   S )Nr    r   rH   r   rU   rt   r   s    r'   r   z'redact_sensitive_text.<locals>.<lambda>  s;    aggajjAGGAJJ$4"5AGGAJJ8O8OO r)   c                 r    |                      d          t          |                      d                    z   S )Nr    r   r   r   s    r'   r   z'redact_sensitive_text.<locals>.<lambda>  s'    aggajj;qwwqzz#:#:: r)   c                 h    |                      d          pd}|                      d          }| | dS )Nr    rH   r   z:***r   )r@   r   digitss      r'   _redact_telegramz/redact_sensitive_text.<locals>._redact_telegram  s9    WWQZZ%2FWWQZZF*f****r)   BEGINz-----z[REDACTED PRIVATE KEY]c                    |                      d          }|                    d          r*|                    d          r|                      d          S |                      d           d|                      d           S )Nr   {}r   r    rg   r   )rU   r   endswith)r@   pws     r'   
_redact_dbz)redact_sensitive_text.<locals>._redact_db  sp    WWQZZ==%% &"++c*:*: &771::%''!**55555r)   c                 \    |                      d           d|                      d           S )Nr    rg   r   r   r   s    r'   r   z'redact_sensitive_text.<locals>.<lambda>  s(    1771::0N0N!''!**0N0N r)   c                     |                      d           t          |                      d                     |                      d           S )Nr    r   r   r   r   s    r'   r   z'redact_sensitive_text.<locals>.<lambda>  s<    J[%<%<JaggajjJJ r)   eyJc                 F    t          |                     d                    S )Nr   )rt   rU   r   s    r'   r   z'redact_sensitive_text.<locals>.<lambda>  s    [%<%< r)   rw   +c                     |                      d          }t          |          dk    r|d d         dz   |dd          z   S |d d         dz   |dd          z   S )Nr       r   z****re   )rU   r$   )r@   phones     r'   _redact_phonez,redact_sensitive_text.<locals>._redact_phone  s`    GGAJJE5zzQRaRy6)E"##J66!9v%bcc
22r)   )
isinstancer   _REDACT_ENABLED_has_known_prefix_substringr   rt   rd   rT   rQ   _ENV_ASSIGN_RE_ENV_ASSIGN_LOWER_RE_CFG_SECRET_WORD_RErV   _CFG_DOTTED_RE_CFG_ANCHORED_RE_JSON_FIELD_RE_YAML_ASSIGN_RE_AUTH_HEADER_RE_SECRET_HEADER_RE_TELEGRAM_RE_PRIVATE_KEY_RE_DB_CONNSTR_RE_URL_BARE_TOKEN_RE_JWT_REr   r   _SIGNAL_PHONE_RE)rF   r   r   r   r   r   r   r   r   r   r   r   s              @r'   r   r     s   Z |tdC   4yy  _   	 #4(( 	G1:K-- *$<<~~????FF  G;$;;D D D "%%k488D D  +//TBB D  %8%?%?%E%E %))+t<<'++K>> $;;3$;;8 8 8 "%%lD99D
 $;;5,,9 9 9 #&&|T::D
 4!7!7""OO
 
 d{{ $$::
 
 d{{	+ 	+ 	+  0$77 $7d??""#;TBB }} 	V6 6 6
 "%%j$77DD!%%&N&NPTUUD "%%JJ
 
 }}{{<<dCC   4-d33 d{{sd{{ && d{{	3 	3 	3
  ##M488Kr)   >   envsetexportdeclareprintenv>   nlbatcattacrh   lessmoreri   typeviewzcatbatcatcommandc                    | sdS t          j        d|           }|D ]}|                                }|s|                                }|r|d         t          vr?|dd         D ]}|                    d          r|                    d          }|                    dd          d	                             d
d          d	         }|                                t          v r  dS dS )ud  Return True if ``command`` reads a ``.env`` file to stdout.

    Detects file-read commands (``cat``, ``head``, ``tail``, etc.) where any
    argument's basename is a ``.env``-style file. Template files
    (``.env.example``, ``.env.sample``, ...) are not in the basename list and
    therefore never match. Handles pipelines and command sequences.

    Conservative defense-in-depth, not a boundary — indirect reads
    (``sudo cat .env``, ``/bin/cat .env``, ``$(cat .env)``, redirection,
    ``sed``/``awk``/``xxd`` readers) are not detected, matching the
    precedent of ``is_env_dump_command`` below.
    F[|;&]+r   r    Nr   z"'/\T)r   ry   r   _FILE_READ_COMMANDSr   rsplitr{   _ENV_FILE_BASENAMES)r  segmentssegtokensargbasenames         r'   _command_reads_env_filer    s     ux	7++H  iikk 	
  	*===!"": 		 		C~~c""  ))E""Czz#q))"-44T1==bAH~~#666ttt 7		 5r)   c                 6   | rt          | t                    sdS t          j        d|           }|D ]g}|                                }|s	 t          j        |          }n$# t          $ r |                                }Y nw xY w|r|d         t          v r dS hdS )a  Return True if ``command`` dumps environment variables to stdout.

    Detects ``env`` / ``printenv`` / ``set`` / ``export`` / ``declare`` as the
    first token of any segment in a pipeline or sequence (``;`` / ``&&`` /
    ``||`` / ``|``). Conservative: a parse failure or anything unrecognized
    returns False (callers then fall back to the safer code_file=True path,
    which still masks prefix-shaped keys).
    Fr  r   T)r   r   r   ry   r   shlex
ValueError_ENV_DUMP_COMMANDS)r  r  r  r  s       r'   is_env_dump_commandr#  5  s      *Wc22 ux	7++H 	 	iikk 		![%%FF 	! 	! 	!YY[[FFF	! 	fQi#555445s   A  B B)r   outputc                t    | s| S |pd}t          |          pt          |           }t          | ||          S )u^  Redact secrets from terminal/process stdout.

    Single redaction policy for ALL terminal-output surfaces — foreground
    ``terminal`` results AND background ``process(action=poll/log/wait)``
    output — so they can't diverge. Picks ``code_file`` based on whether
    ``command`` is an environment dump or reads a ``.env`` file:

    - env-dump command (``env``/``printenv``/``set``/``export``/``declare``)
      → ``code_file=False`` so the ENV-assignment pass masks opaque tokens.
    - file-read command targeting a ``.env`` file (``cat .env``,
      ``head .env.local``, etc.) → ``code_file=False`` for the same reason.
      Per AGENTS.md, ``.env`` files contain only secrets, so the generic
      ENV pass is the right one (keys whose names carry no secret keyword
      can still slip through it — same limit as the env-dump path).
    - anything else (or unknown command) → ``code_file=True`` to avoid
      false positives on source/config dumps.

    ``force=True`` bypasses the global ``security.redact_secrets`` preference
    for safety boundaries that must never emit raw credentials.
    rH   )r   r   )r#  r  r   )r$  r  r   cmdr   s        r'   redact_terminal_outputr'  O  sM    .  
-RC(--M1H1M1MNI u	JJJJr)   patternc                 T    d}t          |           D ]\  }}||v r| d|         c S | S )aG  Return the leading literal characters of a regex pattern.

    Stops at the first regex metacharacter (``[``, ``(``, ``\``, ``.``,
    ``?``, ``*``, ``+``, ``|``, ``{``, ``^``, ``$``).  Returns the literal
    that any match of the pattern MUST contain as a substring, so the
    pre-screen never produces false negatives.
    z[(\.?*+|{^$N)rR   )r(  metar   chs       r'   _extract_literal_prefixr,  x  sK     D7##  2::2A2; Nr)   c                    d}d}|t          |           k     r| |         }|dk    r|dz  }'|dk    r~|dz  }|t          |           k     r| |         dk    r|dz  }|t          |           k     rA| |         dk    r5| |         dk    r|dz  }|dz  }|t          |           k     r| |         dk    5n4|dk    r|dz  }n(|dk    rt          d|dz
            }n|d	k    r|dk    rd
S |dz  }|t          |           k     dS )a  True if ``pattern`` contains a ``|`` outside any group or class.

    A top-level alternation defeats the literal-prefix guarantee:
    ``_extract_literal_prefix`` stops at ``|``, so for ``ab|.*`` it
    returns ``ab`` even though the ``.*`` branch is not bound by that
    prefix and matches anything. Grouped alternation after the prefix
    (``ab(?:x|y)``) keeps the guarantee and stays allowed.
    r   r  r   [r    ]rB   )rD   TF)r$   max)r(  depthr   r+  s       r'   _has_top_level_alternationr3    sF    E	A
c'll

QZ::FA99FA3w<<GAJ#$5$5Qc'll""wqzS'8'81:%%FAQ c'll""wqzS'8'8 3YYQJEE3YY519%%EE3YY5A::4	Q' c'll

( 5r)   c                     dt           dt          f fd}dg}d}|t                     k     rH |         }|dk    r|dz  }(|dk    r~|d	z  }|t                     k     r |         d
k    r|d	z  }|t                     k     rA |         d
k    r5 |         dk    r|d	z  }|d	z  }|t                     k     r |         d
k    5n|dk    r|                    d           n{|dk    rIt          |          d	k    r|                                nd}|r ||d	z             rdS |d         p||d<   n, ||          r!d|d<   |dk    r                     d|          }|d	z  }|t                     k     HdS )u'  True if an unbounded quantifier applies to a group containing one.

    ``(a+)+``, ``(?:x*)*``, ``(a{2,})+`` — the canonical catastrophic-
    backtracking (ReDoS) shape. Registered patterns run against every log
    line, tool output, and transcript chunk, so a pathological pattern from
    a buggy plugin would stall the host process, not just the plugin.

    Detects structural nesting only; ambiguity between overlapping
    alternation branches (``(a|aa)+``) is not statically detected and
    remains the plugin author's responsibility.
    r,   r   c                    | t                    k    rdS |          dv rdS |          dk    r\                    d|           }|dk    r| dz   |         nd}|d d                                         o|                    d	          S dS )
NFz*+Tr   r   r  r    rH   ,)r$   findisdigitr   )r,   kr_   r(  s      r'   _unbounded_quantifier_followszC_has_nested_unbounded_repeat.<locals>._unbounded_quantifier_follows  s     G51:41:S!$$A'(Bww71q57##BD9$$&&=4==+=+==ur)   Fr   r  r   r.  r    r/  rB   r0  Tr  r   r   )intboolr$   rW   popr7  )r(  r:  contains_unboundedr   r+  inners   `     r'   _has_nested_unbounded_repeatr@    s             	A
c'll

QZ::FA99FA3w<<GAJ#$5$5Qc'll""wqzS'8'81:%%FAQ c'll""wqzS'8'8 3YY%%e,,,,3YY034F0G0G!0K0K&**,,,QVE 66q1u== t%7%;%Dur""**1-- 	)%)r"SyyLLa((	Q1 c'll

2 5r)   c              #   4   K   | ]}t          |          V  d S r8   r,  r4   ps     r'   r9   r9     s<        #$A     r)   c                 D     t           fdt          D                       S )zReturn True if ``text`` contains any known credential prefix substring.

    Used as a cheap pre-check before invoking the expensive ``_PREFIX_RE``.
    c              3       K   | ]}|v V  	d S r8   r2   )r4   rD  rF   s     r'   r9   z._has_known_prefix_substring.<locals>.<genexpr>  s'      55QqDy555555r)   )anyr   r   s   `r'   r   r     s)    
 5555"4555555r)   _PLUGIN_PREFIX_PATTERNSc                  H    d t                                           D             S )z5All plugin-registered patterns in registration order.c                     g | ]	}|D ]}|
S r2   r2   )r4   patternsrD  s      r'   r6   z$_plugin_patterns.<locals>.<listcomp>  s%    QQQ(QQ1AQQQQr)   )rH  valuesr2   r)   r'   _plugin_patternsrM    s#    QQ5<<>>QQQQr)   c                      t           t                      z   } t          j        dd                    |           z   dz             at          d | D                       adS )a  Recompile the prefix alternation and pre-screen substrings.

    ``redact_sensitive_text`` and ``_mask_token_nonreusable`` look these
    globals up at call time, so swapping the module attributes (atomic
    under the GIL) propagates immediately to every caller.
    rC   rD   rE   c              3   4   K   | ]}t          |          V  d S r8   rB  rC  s     r'   r9   z*_rebuild_prefix_matcher.<locals>.<genexpr>  s+      LLa6q99LLLLLLr)   N)_PREFIX_PATTERNSrM  r   compilerY   rT   tupler   )combineds    r'   _rebuild_prefix_matcherrT    se      "2"4"44H(!3!336KK J LL8LLLLLr)   pluginsourcec                 `   g }| pg D ]}t          |t                    r|                                st                              d|           H|                                }	 t          j        |           n9# t
          j        $ r'}t                              d|||           Y d}~d}~ww xY wt          |          rt                              d||           t          |          rt                              d||           t          t          |                    dk     rt                              d||           B|t          v s|t                      v s||v rt                              d||           }|                    |           |rt           5  t"                              |g                               |           t)                       ddd           n# 1 swxY w Y   t                              d	|t          |                     t          |          S )
u  Additively register credential-token regexes with the redaction engine.

    Each accepted pattern joins the vendor-prefix alternation used by
    ``redact_sensitive_text`` (same masking, same head/tail rules, same
    non-reusable sentinel on ``file_read``) — everywhere built-in patterns
    apply: logs, terminal output, transport errors, transcripts.

    Per-pattern validation (invalid entries are warned and skipped, never
    raised — a broken plugin must not break startup):

    * must be a non-empty string that compiles as a regex;
    * must not contain a top-level alternation (``ab|.*`` would escape
      the literal-prefix guarantee below through its unprefixed branch;
      grouped alternation after the prefix, ``ab(?:x|y)``, is allowed);
    * must not nest unbounded quantifiers (``(a+)+``-style patterns can
      backtrack catastrophically, and registered patterns run against
      every log line and tool output — see
      ``_has_nested_unbounded_repeat``);
    * must start with at least 2 literal characters (the pre-screen
      substring gate in ``_has_known_prefix_substring`` needs a literal
      anchor; it also structurally rules out redact-everything patterns
      like ``.*``);
    * duplicates of built-in or already-registered patterns are skipped.

    Args:
        patterns: iterable of regex strings (e.g. ``[r"nvapi-[A-Za-z0-9_-]{20,}"]``).
        source: attribution label for log lines (e.g. ``"plugin:my-plugin"``).

    Returns:
        The number of patterns actually accepted.
    z/%s: skipping empty/non-string redaction patternz.%s: skipping invalid redaction pattern %r (%s)Nu   %s: skipping redaction pattern %r — top-level alternation escapes the literal-prefix guarantee (in 'ab|.*' the prefix binds only the first branch); wrap alternation in a group after the prefix, e.g. 'ab(?:x|y)'u   %s: skipping redaction pattern %r — nested unbounded quantifiers (e.g. '(a+)+') can backtrack catastrophically, and registered patterns run on every log line and tool outputr   u~   %s: skipping redaction pattern %r — must start with at least 2 literal characters (needed for the pre-screen substring gate)z+%s: redaction pattern %r already registeredz&%s: registered %d redaction pattern(s))r   r   r   loggerwarningr   rQ  errorr3  r@  r$   r,  rP  rM  debugrW   _registry_lockrH  
setdefaultextendrT  info)rK  rV  acceptedr(  excs        r'   register_redaction_patternsrb    s   @ H>r *! *!'3'' 	w}} 	NNLfUUU--//	Jwx 	 	 	NN@   HHHH	 &g.. 	NN=    '00 	NN    &w//00144NN" 	   &&&'5E5G5G*G*G7V^K^K^LLFPWXXX     
 	& 	&#..vr::AA(KKK#%%%	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	4fc(mm	
 	
 	
 x==s*   $A99B/B**B/$=G--G14G1c                      t           5  t                                           t                       ddd           dS # 1 swxY w Y   dS )z:Drop all plugin-registered patterns (tests/teardown only).N)r\  rH  clearrT  r2   r)   r'    _reset_plugin_redaction_patternsre  q  s    	 " "%%'''!!!" " " " " " " " " " " " " " " " " "s   (=AA)	zGET zPOST zPUT zPATCH zDELETE zHEAD zOPTIONS zTRACE zCONNECT c                 l    |                                  t          fdt          D                       S )z?Cheap pre-check before scanning for access-log request targets.c              3       K   | ]}|v V  	d S r8   r2   )r4   methoduppers     r'   r9   z-_has_http_method_substring.<locals>.<genexpr>  s'      EE6vEEEEEEr)   )ri  rG  _HTTP_METHOD_SUBSTRINGS)rF   ri  s    @r'   _has_http_method_substringrk    s4    JJLLEEEEE-DEEEEEEr)   c                   B     e Zd ZdZd fd	Zdej        def fdZ xZ	S )	RedactingFormatterz9Log formatter that redacts secrets from all log messages.N%c                 @     t                      j        |||fi | d S r8   )super__init__)selffmtdatefmtstylekwargs	__class__s        r'   rq  zRedactingFormatter.__init__  s,    gu7777777r)   recordr   c                 d    t                                          |          }t          |          S r8   )rp  formatr   )rr  rx  originalrw  s      r'   rz  zRedactingFormatter.format  s&    77>>&))$X...r)   )NNrn  )
__name__
__module____qualname____doc__rq  logging	LogRecordr   rz  __classcell__)rw  s   @r'   rm  rm    sr        CC8 8 8 8 8 8/W. /3 / / / / / / / / / /r)   rm  r8   )r   N)rU  )gr  r  osr   r   	threadingurllib.parser   agent.file_safetyr   r  	getLoggerr|  rX  	frozensetr|   _SENSITIVE_BODY_KEYSgetenvr{   r   rP  _SECRET_ENV_NAMESrQ  r   
IGNORECASEr   _SECRET_CFG_NAMES
_CFG_VALUEr   r   r   	MULTILINEr   _YAML_CFG_NAMESr   r;   r   r;  r<  r(   r/   rA   _JSON_KEY_NAMESr   r   _SECRET_HEADER_NAMESr   r   r   r   r   r   r   r   r   r   r   r   r   rJ   rO   rY   rT   rd   ro   rp   rt   r   r   r   r   r   objectr   r   r   r   r   r"  r  r  r#  r'  r,  r3  r@  rR  r   r   rH  dict__annotations__Lockr\  rS   rM  rT  rb  re  rj  rk  	Formatterrm  r2   r)   r'   <module>r     s&
      				 				      % % % % % % T S S S S S		8	$	$
 $) % % %   , !y " " "   4 ")3V<<BBDDHbb< < < P ]  U+UUU  "rz}M  , W /

 !bj!2BMBB  "rz7   +<  * 	  M	  2:`0A``T^``MBL    P "*aaaaMBL  < "*AM c c d      :>   C C $ $         4 Z-/---M " "*FM  [  BJ,	,,,M   rz* 
 "*O  VM (  RZ M	   "*%  2:ABB 
  RZ   2:2   "rz5    %"*   !+
M! !  
O  BJ<   IG  
 RZSXX&6777:OO 

1S 1c 1 1 1 1p !bjF   2/ 2/ 2/2/ 2/ 	2/
 2/ 2/ 2/ 	2/ 2/ 2/ 2/j8s 8s 8 8 8 8     ,.3 .3 . . . . 	s 	s 	 	 	 	0C 0C 0 0 0 0? ? ? ? ? ?0& S    293 93 9 9 9 9.C .C . . . . J3 J3 J J J J< #(k k k
k k 	k
 k !k 	k k k kj YNNNOO   i ! ! !   &S4Z &D & & & &Rt     6 (,K>CK K KK*K7;KK K K KRS S         D8# 8$ 8 8 8 8v U  (8     
6c 6d 6 6 6 62 !#  " " "!!R$ R R R R
M M M MT T# TS T T T Tn" " " "
 FS FT F F F F/ / / / /* / / / / /r)   