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
29 changes: 29 additions & 0 deletions apps/desktop/loopx-control-plane/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ unknown; this is maintenance of the App's own installation, not proof that one
source revision is newer. A provably newer runtime remains selected. Both local HTTP services must expose the selected
artifact's identity before the workspace opens.

If anonymous GitHub ancestry lookup is unavailable, the App can reuse an
existing GitHub CLI login for the same fixed public-repository GET. It never
starts login or requests account permissions. Each tool has a three-second
process deadline; missing, failed or unauthenticated `gh` leaves ancestry
unknown. A successful non-ordering answer is not retried under another account.
Core still owns wheel qualification; this read neither changes its identity
rules nor grants Goal or account authority.

The bundled runtime carries its Core package version from the archived wheel
metadata. Native validation and ordering use Python's [PEP 440 version format](https://packaging.python.org/en/latest/specifications/version-specifiers/),
including alpha, beta, release candidate, development, post-release and local
Expand Down Expand Up @@ -212,6 +220,11 @@ For native qualification, use a separately identified packaged App with the
real backend: select a local Goal and Agent page, quit normally, reopen, and
check the same selection on the new port. Also check window-close, an explicit
return to Manager and a corrupt preference in that disposable App profile.
Bind the disposable registry and its `common_runtime_root` at the QA launch
entrypoint, and set `LOOPX_USAGE_PING=0` for all synthetic Core invocations:
a caller-only environment can be lost when a window tool or the
system relaunches the bundle. Verify the reopened process's data route before
observing or operating it; never use the installed App as a fault fixture.
Retain Session IDs and active-Turn readbacks before and after; navigation is
not evidence that work was resumed or accepted. Cross-platform and sustained
use require their own qualification.
Expand Down Expand Up @@ -339,6 +352,22 @@ npm run dev

## Validation

For the ancestry fallback, use that isolated packaged App and a qualified Core
fixture. A QA-only invalid `CURL_CA_BUNDLE` can deny curl's public read while the
existing `gh` still reads the real relation. Compare the old and candidate
native binaries with the same corrupt preference and immutable Core fixture;
read both service identities and the saved selection after the window opens.
Also check that unavailable public transports retain a qualified runtime and
that normal reopening preserves the disposable Session. Remove faults from the
QA launcher afterwards; never change the system proxy or certificate store.

The viewport below is a separately packaged macOS QA shell with an empty
synthetic Goal registry. It records a successful workspace opening, not model
execution, model-selection qualification or an installed-product upgrade. The
ancestry change adds no new UI control or configuration step.

![Packaged QA workspace with zero Goals](docs/assets/native-ancestry-qa.jpg)

```bash
cd apps/desktop/loopx-control-plane/src-tauri
cargo fmt --check
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/desktop/loopx-control-plane/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod bundled_runtime;
#[cfg(target_os = "macos")]
mod macos_update;
mod maintenance;
mod runtime_ancestry;
mod runtime_selection;
mod service_endpoints;
mod services;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -678,9 +678,10 @@ fn runtime_state_publishes_own_phase(error: &str) -> bool {
}

fn resume_runtime(app: &AppHandle) -> Result<crate::services::SelectedRuntime, String> {
use crate::runtime_ancestry::compare_official_commits;
use crate::runtime_selection::{
compare_official_commits, is_private_runtime, prefer_current_bundle,
prefer_discovered_runtime, same_release_installation, Selection,
is_private_runtime, prefer_current_bundle, prefer_discovered_runtime,
same_release_installation, Selection,
};
let mut selection = crate::runtime_selection::selected(app)?;
let mut installed = crate::services::runtime_identity_for_executable(&selection.executable);
Expand Down
174 changes: 174 additions & 0 deletions apps/desktop/loopx-control-plane/src-tauri/src/runtime_ancestry.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
//! Official source-ancestry transport; runtime selection retains its policy.
use serde_json::Value;
use std::{cmp::Ordering, process::Command, time::Duration};

pub(crate) fn compare_official_commits(left: &str, right: &str) -> Option<Ordering> {
compare_using(left, right, |command| {
let output = crate::services::timed_output_with_timeout(command, Duration::from_secs(3))?;
output
.status
.success()
.then(|| serde_json::from_slice(&output.stdout).ok())
.flatten()
})
}

fn compare_using(
left: &str,
right: &str,
mut request: impl FnMut(Command) -> Option<Value>,
) -> Option<Ordering> {
if ![left, right]
.iter()
.all(|revision| revision.len() == 40 && revision.bytes().all(|c| c.is_ascii_hexdigit()))
{
return None;
}
// Page two retains the relation without downloading page one's file patches.
let path = format!("repos/loopx-project/loopx/compare/{left}...{right}?per_page=1&page=2");
let url = format!("https://api.github.com/{path}");
let mut command = Command::new("curl");
crate::services::configure_runtime_environment(&mut command);
command.args([
"--fail",
"--silent",
"--show-error",
"--proto",
"=https",
"--max-time",
"2",
"--header",
"Accept: application/vnd.github+json",
&url,
]);
let payload = request(command).or_else(|| {
// The anonymous API can be rate-limited even when a user's existing
// GitHub CLI can read this same public relation. Never start login or
// let GH_HOST redirect the read to another authority.
let mut command = Command::new("gh");
crate::services::configure_runtime_environment(&mut command);
command.env("GH_PROMPT_DISABLED", "1").args([
"api",
"--hostname",
"github.com",
"--method",
"GET",
"--header",
"Accept: application/vnd.github+json",
&path,
]);
request(command)
})?;
match payload["status"].as_str()? {
"ahead" => Some(Ordering::Less),
"behind" => Some(Ordering::Greater),
"identical" => Some(Ordering::Equal),
_ => None,
}
}

#[cfg(test)]
mod tests {
use super::*;
const LEFT: &str = "1111111111111111111111111111111111111111";
const RIGHT: &str = "2222222222222222222222222222222222222222";

#[test]
fn comparison_relation_is_not_a_package_version_or_wall_clock() {
for (status, expected) in [
("ahead", Some(Ordering::Less)),
("behind", Some(Ordering::Greater)),
("identical", Some(Ordering::Equal)),
("diverged", None),
] {
let mut calls = 0;
let actual = compare_using(LEFT, RIGHT, |command| {
calls += 1;
assert_eq!(command.get_program(), "curl");
Some(serde_json::json!({"status":status}))
});
assert_eq!(actual, expected);
assert_eq!(calls, 1);
}
}

#[test]
fn invalid_revisions_do_not_reach_a_process() {
for revision in [
"",
"--help",
"../private",
"111111111111111111111111111111111111111z",
] {
assert_eq!(
compare_using(revision, RIGHT, |_| panic!("must not invoke a tool")),
None
);
}
}

#[test]
fn anonymous_failure_uses_the_same_public_read_without_starting_login() {
let mut calls = 0;
let actual = compare_using(LEFT, RIGHT, |command| {
calls += 1;
if calls == 1 {
assert_eq!(command.get_program(), "curl");
return None;
}
assert_eq!(command.get_program(), "gh");
let args: Vec<_> = command
.get_args()
.map(|arg| arg.to_str().unwrap())
.collect();
assert_eq!(
args,
[
"api",
"--hostname",
"github.com",
"--method",
"GET",
"--header",
"Accept: application/vnd.github+json",
&format!(
"repos/loopx-project/loopx/compare/{LEFT}...{RIGHT}?per_page=1&page=2"
)
]
);
assert!(command
.get_envs()
.any(|(key, value)| key == "GH_PROMPT_DISABLED"
&& value == Some(std::ffi::OsStr::new("1"))));
Some(serde_json::json!({"status":"ahead"}))
});
assert_eq!(actual, Some(Ordering::Less));
assert_eq!(calls, 2);
}

#[test]
fn unavailable_tools_leave_ancestry_unknown() {
let mut calls = 0;
assert_eq!(
compare_using(LEFT, RIGHT, |_| {
calls += 1;
None
}),
None
);
assert_eq!(calls, 2);
}

#[test]
fn a_successful_nonordering_answer_is_not_retried_under_another_account() {
let mut calls = 0;
assert_eq!(
compare_using(LEFT, RIGHT, |_| {
calls += 1;
Some(serde_json::json!({"status":"diverged"}))
}),
None
);
assert_eq!(calls, 1);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -269,47 +269,6 @@ pub(crate) fn prefer_current_bundle(
}
}

pub(crate) fn compare_official_commits(left: &str, right: &str) -> Option<std::cmp::Ordering> {
if ![left, right]
.iter()
.all(|revision| revision.len() == 40 && revision.bytes().all(|c| c.is_ascii_hexdigit()))
{
return None;
}
// GitHub includes file patches only on page one. Page two retains the
// comparison relation without downloading a potentially huge source diff.
// https://docs.github.com/en/rest/commits/commits#compare-two-commits
let url = format!(
"https://api.github.com/repos/loopx-project/loopx/compare/{left}...{right}?per_page=1&page=2"
);
let mut command = std::process::Command::new("curl");
crate::services::configure_runtime_environment(&mut command);
command.args([
"--fail",
"--silent",
"--show-error",
"--proto",
"=https",
"--max-time",
"2",
"--header",
"Accept: application/vnd.github+json",
&url,
]);
let output =
crate::services::timed_output_with_timeout(command, std::time::Duration::from_secs(3))?;
if !output.status.success() {
return None;
}
let payload: Value = serde_json::from_slice(&output.stdout).ok()?;
match payload["status"].as_str()? {
"ahead" => Some(std::cmp::Ordering::Less),
"behind" => Some(std::cmp::Ordering::Greater),
"identical" => Some(std::cmp::Ordering::Equal),
_ => None,
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
Loading