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
1 change: 1 addition & 0 deletions .facts
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,7 @@
- satelle run supports --quiet to suppress the preflight summary for automation @implemented @mvp
- satelle run supports a verbose option that expands preflight output into detailed host, readiness, transport, and provider diagnostics @implemented @mvp
- satelle run and steer automatically run the native Computer Use readiness smoke test during preflight when no valid matching native readiness cache entry exists, including first use after setup @spec @mvp @implemented
- satelle Host capabilities reports native readiness unavailable without resolving native runtime authentication when authoritative storage contains no unexpired successful readiness candidate; a candidate still requires the full authenticated current cache key before reuse @spec @mvp @implemented
- satelle run and steer do not require the user to run satelle doctor --scope computer-use --refresh before first prompt execution @spec @mvp @implemented
- satelle run and steer do not start the prompt turn until native Computer Use readiness passes through a valid cache entry or a live harmless readiness smoke test @spec @mvp @implemented
- satelle native Computer Use readiness checks distinguish pointer click support from click-and-drag support instead of treating a successful click as full pointer readiness @spec @mvp @implemented
Expand Down
92 changes: 92 additions & 0 deletions crates/satelle/src/host/runtime-tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,79 @@ fn host_default_cache_lookup_requires_one_desktop_without_blocking_startup() {
}
}

#[test]
fn host_default_cache_lookup_authenticates_only_live_success_candidates() {
for condition in [
"empty",
"expired",
"future",
"failed",
"mismatch",
"matching",
"invalidated",
] {
let state = crate::host::TestStateDir::new().unwrap();
let adapter = ProviderProbeRecoveryAdapter::new([]);
let runtime = RuntimeHandle::new(Ok(state.path().to_path_buf()), adapter.clone());
let engine = runtime.engine().unwrap();
if condition != "empty" {
let key = ProviderProbeRecoveryAdapter::key();
let now = time::OffsetDateTime::now_utc();
let observed_at = match condition {
"expired" => now - time::Duration::minutes(10),
"future" => now + time::Duration::minutes(10),
_ => now,
};
let evidence = key
.evidence(
"capabilities-candidate",
observed_at,
observed_at + time::Duration::minutes(5),
)
.unwrap();
let mut storage = engine.lock_storage().unwrap();
storage
.store_preflight_successes(
key.adapter(),
key.desktop_binding(),
key.execution_policy(),
&evidence,
None,
)
.unwrap();
let change = match condition {
"failed" => Some(
"UPDATE native_readiness_results SET status = 'failed', failure_reason = 'native_action_failed'",
),
"mismatch" => Some(
"UPDATE native_readiness_results SET native_runtime_version = 'changed-runtime'",
),
_ => None,
};
if let Some(sql) = change {
storage.connection_for_test().execute(sql, []).unwrap();
}
if condition == "invalidated" {
storage.invalidate_all_native_readiness().unwrap();
}
}

assert_eq!(
runtime.has_reusable_readiness(LOCAL_DEMO_HOST).unwrap(),
condition == "matching",
"{condition}"
);
assert_eq!(
adapter.readiness_key_calls.load(Ordering::SeqCst),
usize::from(matches!(condition, "matching" | "mismatch")),
"{condition}"
);
assert_eq!(adapter.native_probe_calls.load(Ordering::SeqCst), 0);
assert_eq!(adapter.provider_probe_calls.load(Ordering::SeqCst), 0);
assert_eq!(runtime.snapshot().unwrap().session_count(), 0);
}
}

#[test]
fn host_default_cache_lookup_treats_provider_opt_in_as_unavailable() {
let state = crate::host::TestStateDir::new().expect("temporary state directory should exist");
Expand All @@ -810,6 +883,22 @@ fn host_default_cache_lookup_treats_provider_opt_in_as_unavailable() {
Ok(state.path().to_path_buf()),
BlockedComputerUseAdapter::new(error),
);
// Exercise the adapter error path, rather than the empty-store shortcut.
let key = ProviderProbeRecoveryAdapter::key();
let evidence = ProviderProbeRecoveryAdapter::new([]).readiness();
runtime
.engine()
.unwrap()
.lock_storage()
.unwrap()
.store_preflight_successes(
key.adapter(),
key.desktop_binding(),
key.execution_policy(),
&evidence,
None,
)
.unwrap();

assert!(
!runtime
Expand Down Expand Up @@ -2724,6 +2813,7 @@ struct TerminalRecoveryAdapter {

#[derive(Clone)]
struct ProviderProbeRecoveryAdapter {
readiness_key_calls: Arc<AtomicUsize>,
observations: Arc<Mutex<VecDeque<RecoveryObservation>>>,
observation_calls: Arc<AtomicUsize>,
native_results: Arc<Mutex<VecDeque<NativeProbeBehavior>>>,
Expand Down Expand Up @@ -2751,6 +2841,7 @@ enum NativeProbeBehavior {
impl ProviderProbeRecoveryAdapter {
fn new(observations: impl IntoIterator<Item = RecoveryObservation>) -> Self {
Self {
readiness_key_calls: Arc::new(AtomicUsize::new(0)),
observations: Arc::new(Mutex::new(observations.into_iter().collect())),
observation_calls: Arc::new(AtomicUsize::new(0)),
native_results: Arc::new(Mutex::new(VecDeque::new())),
Expand Down Expand Up @@ -2923,6 +3014,7 @@ impl ComputerUseAdapter for ProviderProbeRecoveryAdapter {
_host: &str,
provider_intent: &ProviderComputerUseIntent,
) -> Result<Option<ReadinessCacheKey>, SatelleError> {
self.readiness_key_calls.fetch_add(1, Ordering::SeqCst);
if self.require_resolved_explicit_binding
&& provider_intent.model().is_some()
&& provider_intent.provider().is_some()
Expand Down
9 changes: 9 additions & 0 deletions crates/satelle/src/host/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2417,6 +2417,15 @@ impl RuntimeEngine {
if self.provider_policy.desktop_bindings.len() != 1 {
return Ok(false);
}
// First use and setup invalidation leave no evidence to authenticate.
// Do not launch native runtime discovery for this status-only miss.
if !self
.lock_storage()?
.has_native_readiness_candidate(time::OffsetDateTime::now_utc())
.map_err(model::storage_failure)?
{
return Ok(false);
}
let intent = ProviderComputerUseIntent::host_default();
let key = match self.adapter.readiness_cache_key(host, &intent) {
Ok(Some(key)) => key,
Expand Down
23 changes: 23 additions & 0 deletions crates/satelle/src/host/storage/operational.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1075,6 +1075,29 @@ impl Storage {
require_idempotent_write(changed)
}

/// A cheap negative lookup before resolving the authenticated runtime key.
/// A candidate never authorizes work; load_reusable_readiness still checks
/// every current key field before evidence can be reused.
pub(crate) fn has_native_readiness_candidate(
&self,
now: time::OffsetDateTime,
) -> Result<bool, StorageError> {
let host_identity = self.host_identity()?;
self.connection
.query_row(
"SELECT EXISTS(
SELECT 1 FROM native_readiness_results
WHERE host_identity_ref = ?1
AND status = 'passed'
AND observed_at <= ?2
AND expires_at > ?2
)",
params![host_identity.as_str(), unix_timestamp_nanos(now)?],
|row| row.get(0),
)
.map_err(operation_failed)
}

/// Returns only a matching, unexpired success. Failed results remain in
/// the authoritative store for diagnostics but never authorize execution.
pub(crate) fn load_reusable_readiness(
Expand Down
Loading