Skip to content

Commit 3bb2495

Browse files
New1Directionclaude
andcommitted
refactor(execution): real command execution + injected signing key + honest heal
Drops three simulations from the speculative DAG path: (1) SpeculativeScheduler::run no longer fakes success via `!command.contains("fail")` + sleep — it runs each node's command (sh -c) and sets status from the real exit code; (2) sign_merkle_root signs with the injected persistent identity (crate::identity, wired from workers.rs) instead of a throwaway per-run OsRng key; (3) recovery::heal_node_with_context returns honest Ok(false) when there's no compiler stderr + worktree context, instead of a sleep-based 'always healed' fallback. TDD (RED→GREEN per change); ledger conformance vectors unchanged; 103 korg-runtime + 26 korg-registry + 8 korg-core tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6ede607 commit 3bb2495

3 files changed

Lines changed: 80 additions & 45 deletions

File tree

‎crates/korg-runtime/src/execution/dag.rs‎

Lines changed: 67 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -196,13 +196,17 @@ pub struct ExecutionSummary {
196196
pub 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

201204
impl 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
}

‎crates/korg-runtime/src/execution/recovery.rs‎

Lines changed: 9 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -439,39 +439,13 @@ pub async fn heal_node_with_context(
439439
}
440440
}
441441

442-
// Simulation fallback (no worktree context available)
442+
// No compiler stderr + worktree path, or no known pattern matched: there is
443+
// nothing real to repair here. Report honestly that we did not heal — the
444+
// caller decides what to do with a genuine failure. Never fake success.
443445
if let Some(ref tx) = logs_tx {
444-
let _ = tx.send(" [HEAL] Launching isolated sandbox recovery workspace...".to_string());
446+
let _ = tx.send(" [HEAL] No actionable error context — cannot auto-heal".to_string());
445447
}
446-
tokio::time::sleep(tokio::time::Duration::from_millis(60)).await;
447-
if let Some(ref tx) = logs_tx {
448-
let _ = tx.send(
449-
" [HEAL] [Step 1/4: Diagnosis] Found missing dependency or syntax boundary mismatch"
450-
.to_string(),
451-
);
452-
}
453-
tokio::time::sleep(tokio::time::Duration::from_millis(80)).await;
454-
if let Some(ref tx) = logs_tx {
455-
let _ = tx.send(
456-
" [HEAL] [Step 2/4: Patching] Synthesizing localized code correction patch"
457-
.to_string(),
458-
);
459-
}
460-
tokio::time::sleep(tokio::time::Duration::from_millis(40)).await;
461-
if let Some(ref tx) = logs_tx {
462-
let _ = tx.send(
463-
" [HEAL] [Step 3/4: Verification] Running regression and lockfile integration tests"
464-
.to_string(),
465-
);
466-
}
467-
tokio::time::sleep(tokio::time::Duration::from_millis(20)).await;
468-
if let Some(ref tx) = logs_tx {
469-
let _ = tx.send(format!(
470-
" [HEAL] [Step 4/4: Redeploy] Patch committed (elapsed: {:.2?})",
471-
start.elapsed()
472-
));
473-
}
474-
Ok(true)
448+
Ok(false)
475449
}
476450

477451
#[cfg(test)]
@@ -482,14 +456,15 @@ mod tests {
482456
#[tokio::test]
483457
async fn test_recovery_flow() {
484458
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
459+
// Without compiler stderr + a worktree path there is nothing real to
460+
// repair, so heal_node must report that honestly — no fake "healed".
485461
let healed = heal_node("thump test --fail", Some(tx)).await.unwrap();
486-
assert!(healed);
462+
assert!(!healed, "heal_node with no error/worktree context cannot heal");
487463
let mut logs = Vec::new();
488464
while let Ok(log) = rx.try_recv() {
489465
logs.push(log);
490466
}
491-
assert!(!logs.is_empty());
492-
assert!(logs.iter().any(|l| l.contains("[HEAL]")));
467+
assert!(logs.iter().any(|l| l.contains("[HEAL] Intercepting")));
493468
}
494469

495470
#[tokio::test]

‎crates/korg-runtime/src/workers.rs‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -548,7 +548,10 @@ pub async fn build_campaign_dag(
548548
});
549549

550550
// Speculative pre-warm (gated on capability)
551-
let mut scheduler = crate::dag::SpeculativeScheduler::new(dag.clone());
551+
let mut scheduler = crate::dag::SpeculativeScheduler::new(
552+
dag.clone(),
553+
crate::identity::load_or_create_identity(),
554+
);
552555
let _ = scheduler.speculative_warm_boot().await;
553556
if let Some(tx) = tui_tx {
554557
let _ = tx.try_send(crate::tui_bridge::TuiUpdate::Trace(

0 commit comments

Comments
 (0)