@@ -72,6 +72,9 @@ fn git_blame_ffi(dir: String, path: String) -> String
7272@ external ( erlang , "gall_ffi" , "git_show_file" )
7373fn git_show_file_ffi ( dir : String , ref : String , path : String ) -> String
7474
75+ @ external ( erlang , "gall_ffi" , "exec" )
76+ fn exec_ffi ( dir : String , command : String ) -> String
77+
7578@ external ( erlang , "gall_ffi" , "list_gestalt_sessions" )
7679fn list_gestalt_sessions_ffi ( gall_dir : String ) -> String
7780
@@ -383,6 +386,29 @@ fn handle_tool_call(
383386 }
384387 }
385388
389+ // Exec — shell command execution, witnessed as @exec
390+ "exec" -> {
391+ case json . get_string ( args , "command" ) {
392+ Error ( _ ) -> # (
393+ state ,
394+ Some ( make_response (
395+ id ,
396+ content_text ( err_json ( "exec requires command" ) ) ,
397+ ) ) ,
398+ None ,
399+ )
400+ Ok ( command ) -> {
401+ let out = exec_ffi ( state . work_dir , command )
402+ let # ( next_state , exec_frag ) = record_exec ( state , command , out )
403+ # (
404+ next_state ,
405+ Some ( make_response ( id , content_text ( json_string ( out ) ) ) ) ,
406+ exec_frag ,
407+ )
408+ }
409+ }
410+ }
411+
386412 _ -> # (
387413 state ,
388414 Some ( make_response (
@@ -592,6 +618,43 @@ fn record_read(
592618 }
593619}
594620
621+ // ---------------------------------------------------------------------------
622+ // @exec annotation
623+ // ---------------------------------------------------------------------------
624+
625+ /// When the agent runs a shell command through gall, record it as an @exec
626+ /// Fragment. The fragment data carries the command and a hash of the output
627+ /// (not the full output, which could be huge).
628+ fn record_exec (
629+ state : State ,
630+ command : String ,
631+ output : String ,
632+ ) -> # ( State , Option ( fragmentation . Fragment ) ) {
633+ case state . sess {
634+ Idle -> # ( state , None )
635+ Active ( session : s , .. ) as active -> {
636+ let ts = int . to_string ( now ( ) )
637+ let author = case session . config ( s ) {
638+ session . SessionConfig ( author : a , .. ) -> a
639+ }
640+ let fragmentation . Sha ( self : output_hash ) = fragmentation . hash ( output )
641+ let w =
642+ fragmentation . witnessed (
643+ fragmentation . Author ( author ) ,
644+ fragmentation . Committer ( "gall" ) ,
645+ fragmentation . Timestamp ( ts ) ,
646+ fragmentation . Message ( "@exec" ) ,
647+ )
648+ let data = "command: " <> command <> "\n output_sha: " <> output_hash
649+ let r = fragmentation . ref ( fragmentation . hash ( ts <> data ) , "exec" )
650+ let frag = fragmentation . shard ( r , w , data )
651+ let # ( s2 , _ ) = session . act ( s , "@exec" , "command: " <> command )
652+ let next_sess = Active ( .. active , session : s2 )
653+ # ( State ( .. state , sess : next_sess ) , Some ( frag ) )
654+ }
655+ }
656+ }
657+
595658fn path_visibility ( path : String ) -> String {
596659 case string . starts_with ( path , "visibility/private/" ) {
597660 True -> ":private"
0 commit comments