@@ -196,13 +196,17 @@ pub struct ExecutionSummary {
196196pub struct SpeculativeScheduler {
197197 pub dag : Arc < Mutex < ExecutionDag > > ,
198198 warm_boot_started : bool ,
199+ /// Stable agent identity that signs this execution's Merkle root — injected,
200+ /// not minted per run (see `crate::identity`).
201+ signing_key : ed25519_dalek:: SigningKey ,
199202}
200203
201204impl SpeculativeScheduler {
202- pub fn new ( dag : ExecutionDag ) -> Self {
205+ pub fn new ( dag : ExecutionDag , signing_key : ed25519_dalek :: SigningKey ) -> Self {
203206 Self {
204207 dag : Arc :: new ( Mutex :: new ( dag) ) ,
205208 warm_boot_started : false ,
209+ signing_key,
206210 }
207211 }
208212
@@ -290,10 +294,9 @@ impl SpeculativeScheduler {
290294 }
291295
292296 let node_start = Instant :: now ( ) ;
293- let success = ! node. command . contains ( "fail" ) ;
297+ let success = run_command ( & node. command ) . await ;
294298
295299 if success {
296- tokio:: time:: sleep ( tokio:: time:: Duration :: from_millis ( 50 ) ) . await ;
297300 node. status = NodeStatus :: Success ;
298301 node. certainty = 100.0 ;
299302 if let Some ( ref tx) = logs_tx_clone {
@@ -346,8 +349,8 @@ impl SpeculativeScheduler {
346349 let merkle_root = dag_guard. compute_merkle_root ( ) ;
347350 let dag_json = serde_json:: to_string ( & * dag_guard) . unwrap_or_default ( ) ;
348351
349- // Ephemeral ed25519 attestation of this execution's Merkle root
350- let ( node_pubkey, signature) = sign_merkle_root ( merkle_root. as_bytes ( ) ) ;
352+ // Sign this execution's Merkle root with the agent's persistent identity.
353+ let ( node_pubkey, signature) = sign_merkle_root ( merkle_root. as_bytes ( ) , & self . signing_key ) ;
351354
352355 if let Some ( ref tx) = logs_tx {
353356 let _ = tx. send ( format ! (
@@ -374,16 +377,32 @@ impl SpeculativeScheduler {
374377 }
375378}
376379
377- fn sign_merkle_root ( data : & [ u8 ] ) -> ( Option < String > , Option < String > ) {
380+ fn sign_merkle_root (
381+ data : & [ u8 ] ,
382+ key : & ed25519_dalek:: SigningKey ,
383+ ) -> ( Option < String > , Option < String > ) {
378384 use ed25519_dalek:: Signer ;
379- let mut rng = rand:: rngs:: OsRng ;
380- let key = ed25519_dalek:: SigningKey :: generate ( & mut rng) ;
381385 let sig = key. sign ( data) ;
382386 let pubkey_hex = hex:: encode ( key. verifying_key ( ) . to_bytes ( ) ) ;
383387 let sig_hex = hex:: encode ( sig. to_bytes ( ) ) ;
384388 ( Some ( pubkey_hex) , Some ( sig_hex) )
385389}
386390
391+ /// Run a shell command for real and report whether it exited successfully.
392+ /// Replaces the previous `!command.contains("fail")` simulation — the DAG node's
393+ /// status now reflects the command's actual exit code.
394+ async fn run_command ( command : & str ) -> bool {
395+ match tokio:: process:: Command :: new ( "sh" )
396+ . arg ( "-c" )
397+ . arg ( command)
398+ . output ( )
399+ . await
400+ {
401+ Ok ( output) => output. status . success ( ) ,
402+ Err ( _) => false ,
403+ }
404+ }
405+
387406// =========================================================================
388407// Tests
389408// =========================================================================
@@ -472,14 +491,52 @@ mod tests {
472491 }
473492
474493 #[ tokio:: test]
475- async fn test_speculative_scheduler_run ( ) {
494+ async fn run_command_reflects_real_exit_status ( ) {
495+ assert ! ( run_command( "true" ) . await , "`true` exits 0 → success" ) ;
496+ assert ! ( !run_command( "false" ) . await , "`false` exits non-zero → failure" ) ;
497+ }
498+
499+ #[ tokio:: test]
500+ async fn run_succeeds_when_commands_really_succeed ( ) {
476501 let mut dag = ExecutionDag :: new ( "smoke" ) ;
477502 dag. add_node ( make_node ( "a" , vec ! [ ] ) ) ;
478503 dag. add_node ( make_node ( "b" , vec ! [ "a" ] ) ) ;
479- let mut scheduler = SpeculativeScheduler :: new ( dag) ;
504+ let key = ed25519_dalek:: SigningKey :: generate ( & mut rand:: rngs:: OsRng ) ;
505+ let mut scheduler = SpeculativeScheduler :: new ( dag, key) ;
480506 let summary = scheduler. run ( None ) . await . unwrap ( ) ;
481507 assert ! ( summary. overall_success) ;
482508 assert_eq ! ( summary. status, "done" ) ;
483509 assert ! ( !summary. merkle_root. is_empty( ) ) ;
484510 }
511+
512+ #[ tokio:: test]
513+ async fn run_fails_on_a_real_nonzero_exit_not_a_string_match ( ) {
514+ // The old sim used `success = !command.contains("fail")`, so `false`
515+ // (no "fail" substring, real exit 1) wrongly passed. Real execution must
516+ // fail it — and with no heal context it must stay failed, not fake-heal.
517+ let mut dag = ExecutionDag :: new ( "smoke" ) ;
518+ dag. add_node ( make_node ( "ok" , vec ! [ ] ) ) ;
519+ let mut bad = make_node ( "bad" , vec ! [ ] ) ;
520+ bad. command = "false" . into ( ) ;
521+ dag. add_node ( bad) ;
522+ let key = ed25519_dalek:: SigningKey :: generate ( & mut rand:: rngs:: OsRng ) ;
523+ let mut scheduler = SpeculativeScheduler :: new ( dag, key) ;
524+ let summary = scheduler. run ( None ) . await . unwrap ( ) ;
525+ assert ! ( !summary. overall_success, "a real non-zero exit must fail the DAG" ) ;
526+ }
527+
528+ #[ test]
529+ fn sign_merkle_root_uses_the_given_key_deterministically ( ) {
530+ use ed25519_dalek:: Verifier ;
531+ let key = ed25519_dalek:: SigningKey :: from_bytes ( & [ 7u8 ; 32 ] ) ;
532+ let ( pk, sig) = sign_merkle_root ( b"root-abc" , & key) ;
533+ // The pubkey is the given key's stable identity (not a throwaway), and
534+ // Ed25519 is deterministic so re-signing the same data matches.
535+ assert_eq ! ( pk, Some ( hex:: encode( key. verifying_key( ) . to_bytes( ) ) ) ) ;
536+ let ( _, sig2) = sign_merkle_root ( b"root-abc" , & key) ;
537+ assert_eq ! ( sig, sig2) ;
538+ let raw = hex:: decode ( sig. unwrap ( ) ) . unwrap ( ) ;
539+ let signature = ed25519_dalek:: Signature :: from_slice ( & raw ) . unwrap ( ) ;
540+ assert ! ( key. verifying_key( ) . verify( b"root-abc" , & signature) . is_ok( ) ) ;
541+ }
485542}
0 commit comments