1- //! 宿主事件的局部执行入口;接入只读发现、Stop 指引、Python 编辑快检和暂存面观察。
1+ //! 宿主事件的局部执行入口;接入只读发现、Stop 指引、任务复检、 编辑快检和暂存面观察。
22
33use crate :: check_budget:: parse_check_timeout;
44use crate :: discovery:: discover;
55use crate :: git_index_safety:: observe_index_safety_with_deadline;
66use crate :: hook_plan_command:: parse_request;
7- use crate :: next_command:: read_local_brief;
7+ use crate :: next_command:: { read_local_brief, read_task_brief } ;
88use crate :: python_lint_command:: {
99 annotate_conversation_budget, scan_selected_report_with_deadline,
1010} ;
1111use codeguard_adapters:: legacy_registry;
1212use codeguard_core:: { HookTriggerAction , plan_hook_trigger} ;
13- use codeguard_runtime:: NativeObservation ;
13+ use codeguard_runtime:: { NativeObservation , ProcessSpec , Termination , run_process } ;
1414use serde_json:: { Value , json} ;
15+ use std:: collections:: BTreeMap ;
1516use std:: env;
17+ use std:: ffi:: OsString ;
1618use std:: fs;
1719use std:: io:: Read ;
1820use std:: path:: { Path , PathBuf } ;
@@ -24,12 +26,17 @@ const MAX_REQUEST_BYTES: u64 = 64 * 1024;
2426const MAX_FAST_TIMEOUT_MS : u64 = 120_000 ;
2527const MAX_GUIDANCE_RECORDS : usize = 64 ;
2628const MAX_GUIDANCE_REPORT_BYTES : u64 = 8 * 1024 * 1024 ;
29+ const MAX_VERIFY_OUTPUT_BYTES : usize = 4 * 1024 * 1024 ;
30+ const MAX_VERIFY_OPTION_BYTES : usize = 16 * 1024 ;
31+ const MAX_VERIFY_EVENTS : usize = 128 ;
32+ const MAX_VERIFY_EVENT_BYTES : u64 = 1024 * 1024 ;
2733
2834struct Arguments {
2935 root : PathBuf ,
3036 timeout_ms : u64 ,
3137 ruff_tool : Option < PathBuf > ,
3238 git_tool : Option < PathBuf > ,
39+ verify_options : BTreeMap < String , String > ,
3340}
3441
3542/// 从 `args` 与 stdin 读取宿主事件,按纯路由执行已接入的局部动作。
@@ -72,6 +79,10 @@ pub fn run(args: &[String]) -> ExitCode {
7279 return ExitCode :: from ( 2 ) ;
7380 }
7481 } ;
82+ if plan. action != HookTriggerAction :: VerifyTask && !arguments. verify_options . is_empty ( ) {
83+ eprintln ! ( "任务复检参数仅用于 repair_ready 事件" ) ;
84+ return ExitCode :: from ( 2 ) ;
85+ }
7586 let root = arguments
7687 . root
7788 . canonicalize ( )
@@ -90,6 +101,13 @@ pub fn run(args: &[String]) -> ExitCode {
90101 }
91102 Err ( _) => ( "not_run" , Some ( "guidance_unavailable" ) , Value :: Null , 3 ) ,
92103 } ,
104+ ( HookTriggerAction :: VerifyTask , Some ( root) ) => {
105+ let task_id = plan. task_id . as_deref ( ) . expect ( "核心已校验任务 ID" ) ;
106+ match task_verification_summary ( root, task_id, & arguments) {
107+ Ok ( feedback) => ( "task_verification" , None , feedback, 3 ) ,
108+ Err ( ( reason, exit_code) ) => ( "not_run" , Some ( reason) , Value :: Null , exit_code) ,
109+ }
110+ }
93111 ( HookTriggerAction :: FastFileCheck , Some ( root) )
94112 if plan. target_paths . iter ( ) . all ( |path| path. ends_with ( ".py" ) ) =>
95113 {
@@ -192,7 +210,7 @@ pub fn run(args: &[String]) -> ExitCode {
192210 println ! (
193211 "{}" ,
194212 json!( {
195- "schema_version" : "0.3 .0" , "report_type" : "hook_execution_feedback" ,
213+ "schema_version" : "0.4 .0" , "report_type" : "hook_execution_feedback" ,
196214 "plan" : plan, "execution" : execution, "reason" : reason,
197215 "local_feedback" : feedback, "delivery_decision" : "not_evaluated" ,
198216 "host_blocking_verified" : false , "soft_result_reused" : false
@@ -201,6 +219,192 @@ pub fn run(args: &[String]) -> ExitCode {
201219 ExitCode :: from ( exit_code)
202220}
203221
222+ fn task_verification_summary (
223+ root : & Path ,
224+ task_id : & str ,
225+ arguments : & Arguments ,
226+ ) -> Result < Value , ( & ' static str , u8 ) > {
227+ let deadline = Instant :: now ( ) + Duration :: from_millis ( arguments. timeout_ms ) ;
228+ check_verification_history_budget ( root, task_id) ?;
229+ let brief = read_task_brief ( root, task_id) . map_err ( |_| ( "task_unavailable" , 3 ) ) ?;
230+ let checker_id = brief[ "checker_id" ]
231+ . as_str ( )
232+ . ok_or ( ( "task_unavailable" , 3 ) ) ?;
233+ if ( arguments. ruff_tool . is_some ( )
234+ && !matches ! ( checker_id, "python.ruff" | "python.ruff.doctor" ) )
235+ || arguments
236+ . verify_options
237+ . keys ( )
238+ . any ( |key| !verify_option_matches_checker ( key, checker_id) )
239+ {
240+ return Err ( ( "verification_arguments_invalid" , 3 ) ) ;
241+ }
242+ let executable = env:: current_exe ( ) . map_err ( |_| ( "verification_process_failed" , 4 ) ) ?;
243+ let remaining_ms = deadline
244+ . saturating_duration_since ( Instant :: now ( ) )
245+ . as_millis ( ) ;
246+ if remaining_ms == 0 {
247+ return Err ( ( "request_deadline_exceeded" , 3 ) ) ;
248+ }
249+ let mut args = vec ! [
250+ OsString :: from( "task" ) ,
251+ OsString :: from( "verify" ) ,
252+ OsString :: from( task_id) ,
253+ root. as_os_str( ) . to_owned( ) ,
254+ OsString :: from( "--timeout" ) ,
255+ OsString :: from( format!( "{remaining_ms}ms" ) ) ,
256+ OsString :: from( "--format=json" ) ,
257+ ] ;
258+ if let Some ( ruff_tool) = & arguments. ruff_tool {
259+ args. extend ( [
260+ OsString :: from ( "--ruff-tool" ) ,
261+ ruff_tool. as_os_str ( ) . to_owned ( ) ,
262+ ] ) ;
263+ }
264+ for ( key, value) in & arguments. verify_options {
265+ args. extend ( [ OsString :: from ( key) , OsString :: from ( value) ] ) ;
266+ }
267+ let mut child_env = BTreeMap :: new ( ) ;
268+ for key in [
269+ "PATH" ,
270+ "HOME" ,
271+ "TMPDIR" ,
272+ "CARGO_HOME" ,
273+ "RUSTUP_HOME" ,
274+ "JAVA_HOME" ,
275+ ] {
276+ if let Some ( value) = env:: var_os ( key) {
277+ child_env. insert ( OsString :: from ( key) , value) ;
278+ }
279+ }
280+ let outcome = run_process (
281+ & ProcessSpec {
282+ executable,
283+ args,
284+ cwd : root. to_path_buf ( ) ,
285+ env : child_env,
286+ stdin : None ,
287+ deadline,
288+ output_limit_bytes : MAX_VERIFY_OUTPUT_BYTES ,
289+ } ,
290+ & AtomicBool :: new ( false ) ,
291+ ) ;
292+ match outcome. termination {
293+ Termination :: Exited ( 3 ) => { }
294+ Termination :: Exited ( 2 ) => return Err ( ( "verification_arguments_invalid" , 3 ) ) ,
295+ Termination :: Exited ( 130 ) | Termination :: Cancelled => {
296+ return Err ( ( "request_cancelled" , 130 ) ) ;
297+ }
298+ Termination :: TimedOut | Termination :: DeadlineBeforeStart => {
299+ return Err ( ( "request_deadline_exceeded" , 3 ) ) ;
300+ }
301+ Termination :: OutputLimit => return Err ( ( "verification_output_exceeded" , 3 ) ) ,
302+ _ => return Err ( ( "verification_process_failed" , 4 ) ) ,
303+ }
304+ let report: Value =
305+ serde_json:: from_slice ( & outcome. stdout ) . map_err ( |_| ( "verification_report_invalid" , 4 ) ) ?;
306+ if report[ "report_type" ] != "task_verification_preview"
307+ || report[ "operation" ] != "task_verify"
308+ || report[ "task_id" ] != task_id
309+ || report[ "exit_code" ] != 3
310+ || report[ "authority" ] != "local_unverified"
311+ || report[ "delivery_decision" ] != "not_evaluated"
312+ || !report[ "event_persisted" ] . is_boolean ( )
313+ {
314+ return Err ( ( "verification_report_invalid" , 4 ) ) ;
315+ }
316+ let observation = report[ "observation" ]
317+ . as_str ( )
318+ . filter ( |value| value. len ( ) <= 64 )
319+ . ok_or ( ( "verification_report_invalid" , 4 ) ) ?;
320+ let reason = report[ "reason" ] . as_str ( ) . filter ( |value| value. len ( ) <= 128 ) ;
321+ if !report[ "reason" ] . is_null ( ) && reason. is_none ( ) {
322+ return Err ( ( "verification_report_invalid" , 4 ) ) ;
323+ }
324+ Ok ( json ! ( {
325+ "schema_version" : "0.1.0" , "report_type" : "hook_task_verification_summary" ,
326+ "task_id" : task_id, "checker_id" : checker_id,
327+ "observation" : observation, "event_persisted" : report[ "event_persisted" ] ,
328+ "reason" : reason, "scan_report_available" : report[ "native_scan" ] . is_object( ) ,
329+ "authority" : "local_unverified" , "delivery_decision" : "not_evaluated"
330+ } ) )
331+ }
332+
333+ fn check_verification_history_budget ( root : & Path , task_id : & str ) -> Result < ( ) , ( & ' static str , u8 ) > {
334+ let events = root
335+ . join ( ".codeguard/findings" )
336+ . join ( task_id)
337+ . join ( "events" ) ;
338+ if !events. exists ( ) {
339+ return Ok ( ( ) ) ;
340+ }
341+ if !fs:: symlink_metadata ( & events) . is_ok_and ( |metadata| metadata. file_type ( ) . is_dir ( ) ) {
342+ return Err ( ( "task_unavailable" , 3 ) ) ;
343+ }
344+ let mut total_bytes = 0_u64 ;
345+ for ( index, entry) in fs:: read_dir ( events)
346+ . map_err ( |_| ( "task_unavailable" , 3 ) ) ?
347+ . enumerate ( )
348+ {
349+ if index >= MAX_VERIFY_EVENTS {
350+ return Err ( ( "verification_scope_exceeded" , 3 ) ) ;
351+ }
352+ let entry = entry. map_err ( |_| ( "task_unavailable" , 3 ) ) ?;
353+ let metadata = fs:: symlink_metadata ( entry. path ( ) ) . map_err ( |_| ( "task_unavailable" , 3 ) ) ?;
354+ if !metadata. file_type ( ) . is_file ( ) {
355+ return Err ( ( "task_unavailable" , 3 ) ) ;
356+ }
357+ total_bytes = total_bytes. saturating_add ( metadata. len ( ) ) ;
358+ if total_bytes > MAX_VERIFY_EVENT_BYTES {
359+ return Err ( ( "verification_scope_exceeded" , 3 ) ) ;
360+ }
361+ }
362+ Ok ( ( ) )
363+ }
364+
365+ fn verify_option_matches_checker ( key : & str , checker_id : & str ) -> bool {
366+ if matches ! ( key, "--owner" | "--lease-token" ) {
367+ return true ;
368+ }
369+ match checker_id {
370+ "node.eslint" | "node.eslint.preparation" => matches ! (
371+ key,
372+ "--node-tool" | "--eslint-entry" | "--eslint-version" | "--config" | "--cwd"
373+ ) ,
374+ "node.npm.audit" => matches ! (
375+ key,
376+ "--node-tool"
377+ | "--npm-entry"
378+ | "--npm-version"
379+ | "--userconfig"
380+ | "--globalconfig"
381+ | "--registry"
382+ ) ,
383+ "python.pip_audit" => matches ! ( key, "--pip-audit-tool" | "--pip-audit-version" ) ,
384+ "go.vet" => key == "--go-tool" ,
385+ "rust.cargo_clippy" | "rust.cargo_check" | "rust.cargo_rustdoc" => key == "--cargo-tool" ,
386+ "rust.cargo_audit" => matches ! ( key, "--cargo-audit-tool" | "--rustsec-db" ) ,
387+ "java.checkstyle" | "java.checkstyle.preparation" => {
388+ matches ! ( key, "--java-tool" | "--checkstyle-jar" | "--config" )
389+ }
390+ "java.maven.p3c" => matches ! (
391+ key,
392+ "--maven-tool" | "--java-home" | "--maven-repo" | "--repo-sha256"
393+ ) ,
394+ "java.maven.dependency_check" => matches ! (
395+ key,
396+ "--maven-tool"
397+ | "--java-home"
398+ | "--maven-repo"
399+ | "--repo-sha256"
400+ | "--cve-data-dir"
401+ | "--cve-data-sha256"
402+ ) ,
403+ "python.ruff" | "python.ruff.doctor" => false ,
404+ _ => false ,
405+ }
406+ }
407+
204408fn guidance_summary ( root : & Path ) -> Result < Value , & ' static str > {
205409 let workspace = root. join ( ".codeguard" ) ;
206410 if workspace. exists ( ) {
@@ -296,6 +500,8 @@ fn parse_args(args: &[String]) -> Result<Arguments, String> {
296500 let mut timeout_ms = None ;
297501 let mut ruff_tool = None ;
298502 let mut git_tool = None ;
503+ let mut verify_options = BTreeMap :: new ( ) ;
504+ let mut verify_option_bytes = 0_usize ;
299505 let mut format_seen = false ;
300506 let mut index = 1 ;
301507 while index < args. len ( ) {
@@ -325,6 +531,64 @@ fn parse_args(args: &[String]) -> Result<Arguments, String> {
325531 }
326532 git_tool = Some ( path) ;
327533 }
534+ "--cargo-tool"
535+ | "--cargo-audit-tool"
536+ | "--rustsec-db"
537+ | "--pip-audit-tool"
538+ | "--pip-audit-version"
539+ | "--go-tool"
540+ | "--maven-tool"
541+ | "--java-home"
542+ | "--java-tool"
543+ | "--checkstyle-jar"
544+ | "--config"
545+ | "--maven-repo"
546+ | "--repo-sha256"
547+ | "--cve-data-dir"
548+ | "--cve-data-sha256"
549+ | "--node-tool"
550+ | "--npm-entry"
551+ | "--npm-version"
552+ | "--userconfig"
553+ | "--globalconfig"
554+ | "--registry"
555+ | "--eslint-entry"
556+ | "--eslint-version"
557+ | "--cwd"
558+ | "--owner"
559+ | "--lease-token"
560+ if !verify_options. contains_key ( key) =>
561+ {
562+ if matches ! (
563+ key,
564+ "--cargo-tool"
565+ | "--cargo-audit-tool"
566+ | "--rustsec-db"
567+ | "--pip-audit-tool"
568+ | "--go-tool"
569+ | "--maven-tool"
570+ | "--java-home"
571+ | "--java-tool"
572+ | "--checkstyle-jar"
573+ | "--config"
574+ | "--maven-repo"
575+ | "--cve-data-dir"
576+ | "--node-tool"
577+ | "--npm-entry"
578+ | "--userconfig"
579+ | "--globalconfig"
580+ | "--eslint-entry"
581+ | "--cwd"
582+ ) && !Path :: new ( & value) . is_absolute ( )
583+ {
584+ return Err ( format ! ( "{key} 必须为绝对路径" ) ) ;
585+ }
586+ verify_option_bytes = verify_option_bytes. saturating_add ( key. len ( ) + value. len ( ) ) ;
587+ if value. is_empty ( ) || verify_option_bytes > MAX_VERIFY_OPTION_BYTES {
588+ return Err ( "任务复检参数为空或超预算" . into ( ) ) ;
589+ }
590+ verify_options. insert ( key. to_owned ( ) , value) ;
591+ }
328592 _ => return Err ( format ! ( "不支持或重复的参数:{key}" ) ) ,
329593 }
330594 index += 1 ;
@@ -341,5 +605,6 @@ fn parse_args(args: &[String]) -> Result<Arguments, String> {
341605 timeout_ms,
342606 ruff_tool,
343607 git_tool,
608+ verify_options,
344609 } )
345610}
0 commit comments