99import json
1010import logging
1111import os
12- import re
1312import stat
1413from dataclasses import dataclass , field , fields , replace
1514from datetime import datetime
6059_PLUGIN_VERSION = "1"
6160_MANIFEST_VERSION = 1
6261_DEFAULT_PROJECT = "Harbor"
63- _VERIFIER_SECRET_ASSIGNMENT = re .compile (
64- r"(?i)(?P<prefix>[\"']?\b(?:[A-Za-z0-9_-]*api[-_]?key[A-Za-z0-9_-]*|token|access[-_]?token|"
65- r"auth[-_]?token|refresh[-_]?token|id[-_]?token|[A-Za-z0-9_-]*(?:secret|password|passwd|credential|"
66- r"authorization|cookie)[A-Za-z0-9_-]*)[\"']?\s*[:=]\s*)"
67- r"(?:[\"'][^\"'\r\n]*[\"']|(?:Bearer|Basic)\s+[^\s,;]+|[^\s,;]+)"
68- )
69- _VERIFIER_BEARER_SECRET = re .compile (r"(?i)(\bBearer\s+)[A-Za-z0-9._~+/=-]+" )
70- _VERIFIER_TOKEN_SECRET = re .compile (r"\b(?:sk-|gh[pousr]_|github_pat_|xox[baprs]-)[A-Za-z0-9_-]{8,}\b" , re .IGNORECASE )
7162
7263
7364@dataclass
@@ -180,58 +171,18 @@ def _read_bounded_file(path: Path, max_bytes: int) -> tuple[bytes | None, str |
180171 if before .st_size > max_bytes :
181172 return None , "attachment size limit"
182173 flags = os .O_RDONLY | getattr (os , "O_BINARY" , 0 ) | getattr (os , "O_NOFOLLOW" , 0 ) | getattr (os , "O_NONBLOCK" , 0 )
183- descriptor = os .open (path , flags )
174+ with os .fdopen (os .open (path , flags ), "rb" ) as file_obj :
175+ opened = os .fstat (file_obj .fileno ())
176+ if not stat .S_ISREG (opened .st_mode ) or (opened .st_dev , opened .st_ino ) != (before .st_dev , before .st_ino ):
177+ return None , "unsafe file type"
178+ data = file_obj .read (max_bytes + 1 )
184179 except FileNotFoundError :
185180 return None , None
186181 except OSError as exc :
187182 return None , str (exc )
188- try :
189- opened = os .fstat (descriptor )
190- if not stat .S_ISREG (opened .st_mode ) or (opened .st_dev , opened .st_ino ) != (before .st_dev , before .st_ino ):
191- return None , "unsafe file type"
192- if opened .st_size > max_bytes :
193- return None , "attachment size limit"
194- chunks = []
195- remaining = max_bytes + 1
196- while remaining :
197- chunk = os .read (descriptor , remaining )
198- if not chunk :
199- break
200- chunks .append (chunk )
201- remaining -= len (chunk )
202- data = b"" .join (chunks )
203- if len (data ) > max_bytes :
204- return None , "attachment size limit"
205- return data , None
206- except OSError as exc :
207- return None , str (exc )
208- finally :
209- os .close (descriptor )
210-
211-
212- def _redact_verifier_value (value : Any ) -> tuple [Any , bool ]:
213- """Scrub common credential forms from raw logs and nested CTRF strings."""
214- if isinstance (value , str ):
215- redacted = _VERIFIER_SECRET_ASSIGNMENT .sub (r"\g<prefix>[REDACTED]" , value )
216- redacted = _VERIFIER_BEARER_SECRET .sub (r"\1[REDACTED]" , redacted )
217- redacted = _VERIFIER_TOKEN_SECRET .sub ("[REDACTED]" , redacted )
218- return redacted , redacted != value
219- if isinstance (value , dict ):
220- result = {}
221- changed = False
222- for key , child in value .items ():
223- result [key ], child_changed = _redact_verifier_value (child )
224- changed = changed or child_changed
225- return result , changed
226- if isinstance (value , list ):
227- result = []
228- changed = False
229- for child in value :
230- redacted_child , child_changed = _redact_verifier_value (child )
231- result .append (redacted_child )
232- changed = changed or child_changed
233- return result , changed
234- return value , False
183+ if opened .st_size > max_bytes or len (data ) > max_bytes :
184+ return None , "attachment size limit"
185+ return data , None
235186
236187
237188def _read_json_summary (entries : list [tuple [str | None , Path ]], max_bytes : int ) -> tuple [Any , list [str ]]:
@@ -340,52 +291,58 @@ def _attachment(
340291 )
341292
342293
294+ _VERIFIER_OUTPUT_FILES = (
295+ ("stdout" , "test-stdout.txt" , False ),
296+ ("stderr" , "test-stderr.txt" , False ),
297+ ("ctrf" , "ctrf.json" , True ),
298+ )
299+
300+
301+ def _decode_verifier_output (data : bytes , parse_json : bool ) -> tuple [Any , list [str ]]:
302+ if not parse_json :
303+ return data .decode ("utf-8" , errors = "replace" ), []
304+ try :
305+ return json .loads (data ), []
306+ except (UnicodeDecodeError , json .JSONDecodeError ):
307+ return data .decode ("utf-8" , errors = "replace" ), ["is not valid JSON" ]
308+
309+
310+ def _read_verifier_output (
311+ path : Path , parse_json : bool , config : PluginConfig , max_bytes : int
312+ ) -> tuple [Any | None , int , list [str ]]:
313+ data , read_warning = _read_bounded_file (path , max_bytes )
314+ if data is None :
315+ return None , 0 , [] if read_warning is None else [f"omitted: { read_warning } " ]
316+ if not data :
317+ return None , 0 , []
318+ value , warnings = _decode_verifier_output (data , parse_json )
319+ normalized = normalize_json (
320+ value ,
321+ max_bytes = config .max_attachment_bytes ,
322+ redact_patterns = config .redact_patterns ,
323+ max_depth = 20 ,
324+ redact_absolute_paths = False ,
325+ )
326+ return normalized .value , len (data ), [* warnings , * normalized .warnings ]
327+
328+
343329def _verifier_output_attachment (result : Any , config : PluginConfig ) -> tuple [Attachment | None , Any , list [str ]]:
344330 if config .attachments == "none" :
345331 return None , None , []
346-
347- output_files = (
348- ("stdout" , "test-stdout.txt" , False ),
349- ("stderr" , "test-stderr.txt" , False ),
350- ("ctrf" , "ctrf.json" , True ),
351- )
352332 outputs : list [tuple [str | None , dict [str , Any ]]] = []
353333 warnings : list [str ] = []
354334 total = 0
355335 for step_name , verifier_dir in verifier_output_paths (result ):
356336 step_output : dict [str , Any ] = {}
357- for key , filename , parse_json in output_files :
337+ for key , filename , parse_json in _VERIFIER_OUTPUT_FILES :
358338 path = verifier_dir / filename
359339 label = _step_label (step_name , path )
360340 limit = min (config .max_attachment_bytes , config .max_total_attachment_bytes - total )
361- data , read_warning = _read_bounded_file (path , limit )
362- if data is None :
363- if read_warning is not None :
364- warnings .append (f"{ label } omitted: { read_warning } " )
365- continue
366- if not data :
367- continue
368- if parse_json :
369- try :
370- value : Any = json .loads (data )
371- except (UnicodeDecodeError , json .JSONDecodeError ):
372- warnings .append (f"{ label } is not valid JSON" )
373- value = data .decode ("utf-8" , errors = "replace" )
374- else :
375- value = data .decode ("utf-8" , errors = "replace" )
376- value , redacted = _redact_verifier_value (value )
377- if redacted :
378- warnings .append (f"{ label } : redacted credential-like text" )
379- normalized = normalize_json (
380- value ,
381- max_bytes = config .max_attachment_bytes ,
382- redact_patterns = config .redact_patterns ,
383- max_depth = 20 ,
384- redact_absolute_paths = False ,
385- )
386- warnings .extend (f"{ label } : { warning } " for warning in normalized .warnings )
387- step_output [key ] = normalized .value
388- total += len (data )
341+ value , size , file_warnings = _read_verifier_output (path , parse_json , config , limit )
342+ warnings .extend (f"{ label } { warning } " for warning in file_warnings )
343+ if value is not None :
344+ step_output [key ] = value
345+ total += size
389346 if step_output :
390347 outputs .append ((step_name , step_output ))
391348
@@ -403,6 +360,33 @@ def _verifier_output_attachment(result: Any, config: PluginConfig) -> tuple[Atta
403360 )
404361
405362
363+ def _remaining_attachment_config (config : PluginConfig , used_bytes : int ) -> PluginConfig :
364+ return replace (config , max_total_attachment_bytes = max (0 , config .max_total_attachment_bytes - used_bytes ))
365+
366+
367+ def _attachments_size (attachments : dict [str , Attachment ]) -> int :
368+ return sum (len (attachment .data ) for attachment in attachments .values ())
369+
370+
371+ def _verifier_evidence (result : Any , config : PluginConfig ) -> tuple [dict [str , Any ], list [str ], int ]:
372+ attachment , summary , warnings = _verifier_output_attachment (result , config )
373+ output : dict [str , Any ] = {}
374+ if summary is not None :
375+ output ["verifier_output_summary" ] = normalize_json (
376+ summary ,
377+ max_bytes = config .max_content_bytes ,
378+ redact_patterns = config .redact_patterns ,
379+ redact_absolute_paths = False ,
380+ ).value
381+ if attachment is not None :
382+ output ["verifier_output" ] = attachment
383+ return output , warnings , 0 if attachment is None else len (attachment .data )
384+
385+
386+ def _output_event (output : dict [str , Any ]) -> dict [str , Any ]:
387+ return {} if not output else {"output" : output }
388+
389+
406390class HarborPlugin :
407391 """Harbor plugin that reconciles final trials into Braintrust experiments."""
408392
@@ -846,7 +830,7 @@ def _sync_final_result(self, result: Any) -> None:
846830 execution_input ["extra_instructions" ] = extra_instructions
847831 selected_artifacts , artifact_attachment_warnings = _artifact_attachments (result , self .config )
848832 metadata ["harbor" ]["warnings" ].extend (artifact_attachment_warnings )
849- attachment_bytes = sum ( len ( attachment . data ) for attachment in selected_artifacts . values () )
833+ attachment_bytes = _attachments_size ( selected_artifacts )
850834 agent_span = task .start_span (
851835 name = "agent_execution" ,
852836 type = "task" ,
@@ -879,30 +863,10 @@ def _sync_final_result(self, result: Any) -> None:
879863 if selected_artifacts :
880864 agent_span .log (output = {"artifacts" : selected_artifacts })
881865 agent_span .end (end_time = agent_end )
882- verifier_config = replace (
883- self .config ,
884- max_total_attachment_bytes = max (0 , self .config .max_total_attachment_bytes - attachment_bytes ),
885- )
886- verifier_attachment , verifier_summary , verifier_warnings = _verifier_output_attachment (result , verifier_config )
866+ verifier_config = _remaining_attachment_config (self .config , attachment_bytes )
867+ verifier_output , verifier_warnings , verifier_attachment_bytes = _verifier_evidence (result , verifier_config )
887868 metadata ["harbor" ]["warnings" ].extend (verifier_warnings )
888- if verifier_attachment is not None :
889- attachment_bytes += len (verifier_attachment .data )
890- bounded_verifier_summary = (
891- None
892- if verifier_summary is None
893- else normalize_json (
894- verifier_summary ,
895- max_bytes = self .config .max_content_bytes ,
896- redact_patterns = self .config .redact_patterns ,
897- redact_absolute_paths = False ,
898- ).value
899- )
900- verification_output : dict [str , Any ] = {}
901- if bounded_verifier_summary is not None :
902- verification_output ["verifier_output_summary" ] = bounded_verifier_summary
903- if verifier_attachment is not None :
904- verification_output ["verifier_output" ] = verifier_attachment
905- verification_event = {} if not verification_output else {"output" : verification_output }
869+ attachment_bytes += verifier_attachment_bytes
906870 self ._start_phase (
907871 task ,
908872 result ,
@@ -911,7 +875,7 @@ def _sync_final_result(self, result: Any) -> None:
911875 trial_id ,
912876 root_start ,
913877 root_end ,
914- ** verification_event ,
878+ ** _output_event ( verifier_output ) ,
915879 )
916880
917881 for step in getattr (result , "step_results" , None ) or []:
@@ -979,10 +943,7 @@ def _sync_final_result(self, result: Any) -> None:
979943 task .log (metadata = {"harbor" : {"warnings" : trajectory_warnings }})
980944 task .end (end_time = root_end )
981945
982- details_config = replace (
983- self .config ,
984- max_total_attachment_bytes = max (0 , self .config .max_total_attachment_bytes - attachment_bytes ),
985- )
946+ details_config = _remaining_attachment_config (self .config , attachment_bytes )
986947 details_attachment , details_summary , detail_warnings = _attachment (
987948 reward_details_paths (result ), details_config
988949 )
@@ -1014,10 +975,7 @@ def _sync_final_result(self, result: Any) -> None:
1014975 scorer_output ["reward_details_summary" ] = bounded_details
1015976 if details_attachment is not None :
1016977 scorer_output ["reward_details" ] = details_attachment
1017- if bounded_verifier_summary is not None :
1018- scorer_output ["verifier_output_summary" ] = bounded_verifier_summary
1019- if verifier_attachment is not None :
1020- scorer_output ["verifier_output" ] = verifier_attachment
978+ scorer_output .update (verifier_output )
1021979 scorer .log (output = scorer_output , scores = {score .name : score .value })
1022980 scorer .end (end_time = root_end )
1023981
0 commit comments