Skip to content
Open
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
120 changes: 120 additions & 0 deletions desktop/src-tauri/src/local_eval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,54 @@ its own (contract request 8).
*/
pub const CONNECT_ARGUMENTS: [&str; 4] = ["local", "connect", "--approve-connection", "--json"];

/// Where the CLI records which stack an app folder uses, relative to that folder.
pub const LOCAL_REFERENCE: &str = ".copilotkit/local.json";

/// A stack the CLI recorded for a folder that contains an installation, found by
/// `enclosing_reference`.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EnclosingReference {
/// The folder the stack was set up for: an ancestor of the installation.
pub directory: PathBuf,
/// The stack it names, when the file names one this app can pass back to the CLI.
pub stack: Option<StackId>,
}

/**
The stack another folder's `local setup` left in charge of `installation`, if one is.

`local setup` and `local connect` do not only look in the folder they run in: they look for
`.copilotkit/local.json` there and then in every folder above it, and take the first one they
find as this folder's own. So a stack set up earlier for a folder that contains the installation,
such as the home folder, is resumed by setup and connected by connect. Connect then writes that
folder's `.env`, never the installation's.

`None` when the installation has a reference of its own (the CLI stops there), or no folder above
it has one. Only the reference file is read; nothing of the CLI's private configuration is.
*/
pub fn enclosing_reference(installation: &std::path::Path) -> Option<EnclosingReference> {
// The CLI resolves symbolic links before it looks, so this does too.
let start = std::fs::canonicalize(installation).unwrap_or_else(|_| installation.to_path_buf());
if start.join(LOCAL_REFERENCE).exists() {
return None;
}
let directory = start
.ancestors()
.skip(1)
.find(|folder| folder.join(LOCAL_REFERENCE).exists())?
.to_path_buf();
let stack = std::fs::read_to_string(directory.join(LOCAL_REFERENCE))
.ok()
.and_then(|text| serde_json::from_str::<Value>(&text).ok())
.and_then(|reference| {
reference
.get("stackId")
.and_then(Value::as_str)
.and_then(|id| StackId::parse(id).ok())
});
Some(EnclosingReference { directory, stack })
}

/**
`local login` signs the browser in to the stack's dashboard with no code to approve.

Expand Down Expand Up @@ -4575,4 +4623,76 @@ fn main() {
assert!(bundled_cli().is_none());
assert!(bundled_cli_root().is_some());
}

/// A fresh folder with a canonical path, so the lookup's answer compares by value.
fn reference_root(name: &str) -> PathBuf {
let root = std::env::temp_dir().join(format!(
"openbot-local-reference-{name}-{}-{:016x}",
std::process::id(),
rand::random::<u64>()
));
std::fs::create_dir_all(&root).unwrap();
std::fs::canonicalize(root).unwrap()
}

fn write_reference(folder: &std::path::Path, stack: &str) {
std::fs::create_dir_all(folder.join(".copilotkit")).unwrap();
std::fs::write(
folder.join(LOCAL_REFERENCE),
serde_json::json!({ "stackId": stack, "appPath": folder }).to_string(),
)
.unwrap();
}

#[test]
fn a_stack_set_up_for_a_containing_folder_is_found() {
// Found trying OpenBot Desktop on a machine where the CLI had been tested in the home
// folder: the CLI took that stack as the installation's own.
let home = reference_root("enclosing");
let installation = home.join("OpenBot");
std::fs::create_dir_all(&installation).unwrap();
write_reference(&home, "local-9e636d796eff89a28e631942");
assert_eq!(
enclosing_reference(&installation),
Some(EnclosingReference {
directory: home.clone(),
stack: Some(StackId::parse("local-9e636d796eff89a28e631942").unwrap()),
})
);
// A folder that does not exist yet is looked up from where it would be.
assert_eq!(
enclosing_reference(&installation.join("not-yet")).map(|found| found.directory),
Some(home.clone())
);
let _ = std::fs::remove_dir_all(home);
}

#[test]
fn an_installation_with_its_own_stack_or_none_above_it_has_no_enclosing_one() {
let home = reference_root("own");
let installation = home.join("OpenBot");
std::fs::create_dir_all(&installation).unwrap();
assert_eq!(enclosing_reference(&installation), None);
write_reference(&home, "local-abc123");
write_reference(&installation, "local-def456");
assert_eq!(enclosing_reference(&installation), None);
let _ = std::fs::remove_dir_all(home);
}

#[test]
fn an_unreadable_enclosing_reference_is_still_found_without_a_stack() {
let home = reference_root("unreadable");
let installation = home.join("OpenBot");
std::fs::create_dir_all(home.join(".copilotkit")).unwrap();
std::fs::create_dir_all(&installation).unwrap();
std::fs::write(home.join(LOCAL_REFERENCE), "{\"stackId\":\"--force\"}").unwrap();
assert_eq!(
enclosing_reference(&installation),
Some(EnclosingReference {
directory: home.clone(),
stack: None
})
);
let _ = std::fs::remove_dir_all(home);
}
}
114 changes: 114 additions & 0 deletions desktop/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5498,9 +5498,38 @@ fn apps_unavailable() -> Problem {
openbot_desktop_lib::composio::setup_problem()
}

/**
What the window says when a folder containing `root` already has a stack of the CLI's, which the
CLI would use as `root`'s own (see `local_eval::enclosing_reference`). Neither setup nor connect
runs then: setup would resume that stack and OpenBot would record it as one it made, and connect
would write that folder's settings, never the installation's.
*/
fn enclosing_stack_problem(found: &local_eval::EnclosingReference) -> Problem {
let delete = match &found.stack {
Some(stack) => format!(
"copilotkit local delete --stack {0} --confirm-delete {0}",
stack.as_str()
),
None => "copilotkit local delete".to_owned(),
};
Problem::with(
format!(
"{} already has a back end that the CopilotKit CLI set up, and OpenBot's folder is inside it. OpenBot can't set up its own back end there. Delete that back end and its data with {delete}, then choose Retry.",
found.directory.display()
),
found
.directory
.join(local_eval::LOCAL_REFERENCE)
.display()
.to_string(),
)
}

/**
Run `setup` in `root` and record what it made.

Not when a folder containing `root` already has a stack: see `enclosing_stack_problem`.

A stack set up here was made for OpenBot, so it starts and stops with it. The one exception is a
record that already calls the same stack shared: setting up again resumes that stack, it does not
make OpenBot its owner.
Expand All @@ -5509,6 +5538,9 @@ fn set_up_local_eval_at<S>(root: &Path, setup: S) -> Result<local_eval::Report,
where
S: FnOnce(&Path) -> Result<(local_eval::Report, String), local_eval::Failure>,
{
if let Some(found) = local_eval::enclosing_reference(root) {
return Err(SetupError::Unrecorded(enclosing_stack_problem(&found)));
}
let (report, cli_sha256) = setup(root).map_err(SetupError::Failure)?;
let Some(stack) = report
.stack
Expand Down Expand Up @@ -5592,6 +5624,9 @@ where
let unconnected = |said: &str| ConnectError::Unconnected(Problem::plain(said));
let recorded = local_eval_state::read(root, None)
.ok_or_else(|| unconnected("Set up the back end before connecting OpenBot to it."))?;
if let Some(found) = local_eval::enclosing_reference(root) {
return Err(ConnectError::Unconnected(enclosing_stack_problem(&found)));
}
let connection = connect(root).map_err(ConnectError::Failure)?;
if connection.outcome != local_eval::Outcome::Completed || connection.conflicted {
return Ok(connection);
Expand Down Expand Up @@ -11555,6 +11590,85 @@ mod tests {
let _ = std::fs::remove_dir_all(parent);
}

/// A home folder where the CLI was tested before OpenBot Desktop was installed into it: the
/// CLI's reference for that earlier stack sits above the installation.
fn local_eval_under_an_earlier_stack(
label: &str,
ownership: Option<local_eval_state::Ownership>,
) -> (PathBuf, PathBuf) {
let home = temp_root(label);
std::fs::create_dir_all(home.join(".copilotkit")).unwrap();
std::fs::write(
home.join(local_eval::LOCAL_REFERENCE),
serde_json::json!({ "stackId": "local-9e636d796eff89a28e631942", "appPath": home })
.to_string(),
)
.unwrap();
let root = home.join("OpenBot");
std::fs::create_dir_all(&root).unwrap();
if let Some(ownership) = ownership {
let stack = local_eval::StackId::parse("local-9e636d796eff89a28e631942").unwrap();
local_eval_state::begin(&root, &stack, ownership, LOCAL_EVAL_PIN).unwrap();
}
(home, root)
}

#[test]
fn setup_under_a_folder_with_its_own_stack_is_refused_before_the_cli_runs() {
let (home, root) =
local_eval_under_an_earlier_stack("openbot-local-eval-setup-enclosed", None);
let error = set_up_local_eval_at(&root, |_| {
panic!("the CLI would resume the home folder's stack as OpenBot's")
})
.expect_err("refused");
let SetupError::Unrecorded(problem) = error else {
panic!("{error:?}");
};
let home_shown = std::fs::canonicalize(&home).unwrap().display().to_string();
assert!(problem.said.starts_with(&home_shown), "{problem:?}");
assert!(
problem.said.contains(
"copilotkit local delete --stack local-9e636d796eff89a28e631942 --confirm-delete local-9e636d796eff89a28e631942"
),
"{problem:?}"
);
assert!(!problem.said.contains("Intelligence"), "{problem:?}");
// Nothing recorded, so OpenBot never calls that stack its own.
assert_eq!(local_eval_state::read(&root, None), None);
let _ = std::fs::remove_dir_all(home);
}

#[test]
fn connect_under_a_folder_with_its_own_stack_is_refused_before_the_cli_runs() {
let (home, root) = local_eval_under_an_earlier_stack(
"openbot-local-eval-connect-enclosed",
Some(local_eval_state::Ownership::Created),
);
let vault = FakeVault::default();
let error = connect_local_eval_at(
&root,
|_| -> Result<local_eval::Connection, local_eval::Failure> {
panic!("the CLI would write the home folder's .env")
},
|_, name, value| {
vault.borrow_mut().insert(name.into(), value.into());
Ok(())
},
|_, name| Ok(vault.borrow().get(name).cloned()),
)
.expect_err("refused");
let ConnectError::Unconnected(problem) = error else {
panic!("{error:?}");
};
assert!(
problem.said.contains("OpenBot's folder is inside it"),
"{problem:?}"
);
assert!(vault.borrow().is_empty());
assert!(!local_eval_state::read(&root, None).unwrap().connected);
let _ = std::fs::remove_dir_all(home);
}

const LOCAL_KEY: &str = "cpk-synthetic-local-key";

/// What `local connect` leaves in an installation folder, after a line OpenBot wrote earlier.
Expand Down
Loading