Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 35 additions & 5 deletions crates/ironclaw_engine/src/executor/orchestrator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -830,14 +830,15 @@ async fn handle_execute_code_step(
"had_error": result.failure.is_some(),
"pending_gate": result.need_approval.as_ref().map(|na| {
match na {
ThreadOutcome::GatePaused { gate_name, action_name, call_id, parameters, resume_kind, resume_output } => serde_json::json!({
ThreadOutcome::GatePaused { gate_name, action_name, call_id, parameters, resume_kind, resume_output, paused_lease } => serde_json::json!({
"gate_paused": true,
"gate_name": gate_name,
"action_name": action_name,
"call_id": call_id,
"parameters": parameters,
"resume_kind": serde_json::to_value(resume_kind).unwrap_or_default(),
"resume_output": resume_output,
"paused_lease": paused_lease,
}),
_ => serde_json::Value::Null,
}
Expand Down Expand Up @@ -1117,6 +1118,7 @@ async fn handle_execute_action(
parameters,
resume_kind,
resume_output,
paused_lease,
}) => {
let _ = leases.refund_use(lease.id).await;
let output = serde_json::json!({"status": "gate_paused", "gate_name": gate_name});
Expand Down Expand Up @@ -1147,6 +1149,7 @@ async fn handle_execute_action(
"parameters": parameters,
"resume_kind": serde_json::to_value(&*resume_kind).unwrap_or_default(),
"resume_output": resume_output,
"paused_lease": paused_lease.as_deref().cloned(),
});
ExtFunctionResult::Return(json_to_monty(&result))
}
Expand Down Expand Up @@ -1624,6 +1627,7 @@ async fn execute_single_action(
parameters,
resume_kind,
resume_output,
paused_lease,
}) => {
let output = serde_json::json!({"status": "gate_paused", "gate_name": &gate_name});
let event = EventKind::ApprovalRequested {
Expand All @@ -1646,6 +1650,7 @@ async fn execute_single_action(
"parameters": parameters,
"resume_kind": serde_json::to_value(&*resume_kind).unwrap_or_default(),
"resume_output": resume_output,
"paused_lease": paused_lease.as_deref().cloned(),
});
(result_json, event, output)
}
Expand Down Expand Up @@ -2541,6 +2546,10 @@ fn parse_outcome(result: &serde_json::Value) -> ThreadOutcome {
.unwrap_or(serde_json::json!({})),
resume_kind,
resume_output: result.get("resume_output").cloned(),
paused_lease: result
.get("paused_lease")
.cloned()
.and_then(|value| serde_json::from_value(value).ok()),
}
}
_ => ThreadOutcome::Completed { response: None },
Expand Down Expand Up @@ -3112,6 +3121,7 @@ mod tests {
parameters: serde_json::json!({"cmd":"ls"}),
resume_kind: crate::gate::ResumeKind::Approval { allow_always: true },
resume_output: None,
paused_lease: None,
};
normalize_pause_outcome(&mut thread, &outcome).unwrap();
assert_eq!(thread.state, ThreadState::Waiting);
Expand All @@ -3133,18 +3143,38 @@ mod tests {

#[test]
fn parse_outcome_gate_paused() {
let lease = crate::types::capability::CapabilityLease {
id: crate::types::capability::LeaseId::new(),
thread_id: crate::types::thread::ThreadId::new(),
capability_name: "test-capability".into(),
granted_actions: crate::types::capability::GrantedActions::Specific(vec![
"shell".into(),
]),
granted_at: chrono::Utc::now(),
expires_at: None,
max_uses: Some(1),
uses_remaining: Some(1),
revoked: false,
revoked_reason: None,
};
let result = serde_json::json!({
"outcome": "gate_paused",
"gate_name": "approval",
"action_name": "shell",
"call_id": "abc",
"parameters": {"cmd": "rm -rf /"},
"resume_kind": {"Approval": {"allow_always": true}}
"resume_kind": {"Approval": {"allow_always": true}},
"paused_lease": lease,
});
let outcome = parse_outcome(&result);
assert!(
matches!(outcome, ThreadOutcome::GatePaused { action_name, .. } if action_name == "shell")
);
assert!(matches!(
outcome,
ThreadOutcome::GatePaused {
action_name,
paused_lease: Some(_),
..
} if action_name == "shell"
));
}

#[test]
Expand Down
1 change: 1 addition & 0 deletions crates/ironclaw_engine/src/executor/scripting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1186,6 +1186,7 @@ async fn preflight_action(
parameters: params.clone(),
resume_kind: crate::gate::ResumeKind::Approval { allow_always: true },
resume_output: None,
paused_lease: None,
},
);
}
Expand Down
10 changes: 10 additions & 0 deletions crates/ironclaw_engine/src/executor/structured.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ pub async fn execute_action_calls(
parameters: call.parameters.clone(),
resume_kind: crate::gate::ResumeKind::Approval { allow_always: true },
resume_output: None,
paused_lease: None,
}),
});
}
Expand Down Expand Up @@ -319,6 +320,11 @@ pub async fn execute_action_calls(
allow_always: false,
}),
resume_output: result.output.get("resume_output").cloned(),
paused_lease: result
.output
.get("paused_lease")
.cloned()
.and_then(|value| serde_json::from_value(value).ok()),
});
}
results.push(result);
Expand Down Expand Up @@ -387,6 +393,7 @@ fn classify_exec_result(
parameters,
resume_kind,
resume_output,
paused_lease,
}) => {
let _error_msg = format!("gate paused: {gate_name}");
let error_result = ActionResult {
Expand All @@ -397,6 +404,7 @@ fn classify_exec_result(
"gate": gate_name,
"resume_kind": serde_json::to_value(&*resume_kind).unwrap_or_default(),
"resume_output": resume_output.as_deref().cloned(),
"paused_lease": paused_lease.as_deref().cloned(),
}),
is_error: true,
duration: std::time::Duration::ZERO,
Expand Down Expand Up @@ -893,6 +901,7 @@ mod tests {
auth_url: None,
}),
resume_output: None,
paused_lease: None,
})],
));
let leases = Arc::new(LeaseManager::new());
Expand Down Expand Up @@ -976,6 +985,7 @@ mod tests {
auth_url: None,
}),
resume_output: None,
paused_lease: None,
}),
Ok(ActionResult {
call_id: String::new(),
Expand Down
5 changes: 5 additions & 0 deletions crates/ironclaw_engine/src/runtime/messaging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ pub enum ThreadOutcome {
/// Completed action output that should be injected on resume instead
/// of re-running the action.
resume_output: Option<serde_json::Value>,
/// Lease snapshot captured when the gate paused the action.
/// Boxed to keep `ThreadOutcome::GatePaused` under clippy's
/// `large_enum_variant` threshold — `CapabilityLease` is ~360
/// bytes and would otherwise dominate the whole enum's size.
paused_lease: Option<Box<crate::types::capability::CapabilityLease>>,
},
}

Expand Down
1 change: 1 addition & 0 deletions crates/ironclaw_engine/src/runtime/mission.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4151,6 +4151,7 @@ mod tests {
allow_always: false,
}),
resume_output: None,
paused_lease: None,
});
}
Ok(ActionResult {
Expand Down
1 change: 1 addition & 0 deletions crates/ironclaw_engine/src/types/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ pub enum EngineError {
parameters: Box<serde_json::Value>,
resume_kind: Box<crate::gate::ResumeKind>,
resume_output: Option<Box<serde_json::Value>>,
paused_lease: Option<Box<crate::types::capability::CapabilityLease>>,
},
}

Expand Down
1 change: 1 addition & 0 deletions src/agent/agent_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1595,6 +1595,7 @@ impl Agent {
submission,
Submission::ExecApproval { .. }
| Submission::ApprovalResponse { .. }
| Submission::ExternalCallback { .. }
| Submission::GateAuthResolution { .. }
) {
message
Expand Down
13 changes: 12 additions & 1 deletion src/bridge/effect_adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ impl EffectBridgeAdapter {
parameters: serde_json::Value,
resume_kind: ironclaw_engine::ResumeKind,
resume_output: Option<serde_json::Value>,
paused_lease: Option<CapabilityLease>,
) -> EngineError {
EngineError::GatePaused {
gate_name: gate_name.to_string(),
Expand All @@ -177,6 +178,7 @@ impl EffectBridgeAdapter {
parameters: Box::new(parameters),
resume_kind: Box::new(resume_kind),
resume_output: resume_output.map(Box::new),
paused_lease: paused_lease.map(Box::new),
}
}

Expand All @@ -185,6 +187,7 @@ impl EffectBridgeAdapter {
parameters: serde_json::Value,
context: &ThreadExecutionContext,
output_value: &serde_json::Value,
lease: &CapabilityLease,
) -> Option<EngineError> {
let status = output_value.get("status").and_then(|v| v.as_str())?;
let name = output_value.get("name").and_then(|v| v.as_str())?;
Expand Down Expand Up @@ -221,6 +224,7 @@ impl EffectBridgeAdapter {
),
},
None,
Some(lease.clone()),
)),
_ => None,
}
Expand Down Expand Up @@ -726,7 +730,7 @@ impl EffectBridgeAdapter {
&self,
action_name: &str,
parameters: serde_json::Value,
_lease: &CapabilityLease,
lease: &CapabilityLease,
context: &ThreadExecutionContext,
approval_already_granted: bool,
) -> Result<ActionResult, EngineError> {
Expand Down Expand Up @@ -826,6 +830,7 @@ impl EffectBridgeAdapter {
auth_url: sanitize_auth_url(auth_url.as_deref()),
},
None,
Some(lease.clone()),
));
}
Ok(crate::bridge::auth_manager::LatentActionExecution::NeedsSetup { message }) => {
Expand Down Expand Up @@ -898,6 +903,7 @@ impl EffectBridgeAdapter {
auth_url: sanitize_auth_url(cred.auth_url.as_deref()),
},
None,
Some(lease.clone()),
));
}
AuthCheckResult::Ready => {
Expand Down Expand Up @@ -939,6 +945,7 @@ impl EffectBridgeAdapter {
auth_url: sanitize_auth_url(auth_url.as_deref()),
},
None,
Some(lease.clone()),
));
}
ToolReadiness::NeedsSetup { message } => {
Expand Down Expand Up @@ -996,6 +1003,7 @@ impl EffectBridgeAdapter {
allow_always: false,
},
None,
Some(lease.clone()),
));
}
}
Expand All @@ -1011,6 +1019,7 @@ impl EffectBridgeAdapter {
parameters,
ironclaw_engine::ResumeKind::Approval { allow_always: true },
None,
Some(lease.clone()),
));
}
}
Expand Down Expand Up @@ -1163,6 +1172,7 @@ impl EffectBridgeAdapter {
parameters.clone(),
context,
&output_value,
lease,
)
{
return Err(err);
Expand Down Expand Up @@ -1204,6 +1214,7 @@ impl EffectBridgeAdapter {
auth_url: None,
},
None,
Some(lease.clone()),
));
}

Expand Down
Loading
Loading