From ac9ab99f5f2326d3296a13209c0bf7d5d94e25d8 Mon Sep 17 00:00:00 2001 From: Manuel Rinaldi Date: Thu, 1 Oct 2026 09:34:50 +0300 Subject: [PATCH 01/55] feat(windows): build native desktop and managed daemon --- .gitattributes | 5 + .github/workflows/windows.yml | 65 + CONTRIBUTING.md | 6 +- README.md | 2 + apps/desktop/src-tauri/src/daemon.rs | 51 +- apps/desktop/src-tauri/src/lib.rs | 8 +- .../desktop/src-tauri/tauri.windows.conf.json | 22 + apps/marketing/public/THIRD_PARTY_NOTICES.txt | 2 +- crates/gravityd/src/activity.rs | 8 +- crates/gravityd/src/backup.rs | 2 + crates/gravityd/src/channel.rs | 62 +- crates/gravityd/src/config.rs | 4 +- crates/gravityd/src/lib.rs | 5 + crates/gravityd/src/main.rs | 4 +- crates/gravityd/src/paths.rs | 17 +- crates/gravityd/src/paths/trust.rs | 54 +- crates/gravityd/src/paths/windows_hooks.rs | 47 + crates/gravityd/src/permissions.rs | 53 + crates/gravityd/src/runtime/double.rs | 36 +- crates/gravityd/src/runtime/inbox.rs | 89 + crates/gravityd/src/runtime/mod.rs | 2 + crates/gravityd/src/runtime/pty.rs | 186 +- crates/gravityd/src/runtime/pty/tests.rs | 158 + crates/gravityd/src/secrets.rs | 5 +- crates/gravityd/src/server/port.rs | 12 +- crates/gravityd/src/server/port/tests.rs | 11 + crates/gravityd/src/service.rs | 1 + crates/gravityd/src/service/windows.rs | 240 + crates/gravityd/tests/activity.rs | 8 +- crates/gravityd/tests/model.rs | 8 +- crates/gravityd/tests/windows_inbox.rs | 40 + crates/gravityd/tests/windows_pty.rs | 41 + docs/windows.md | 60 + package.json | 2 +- scripts/notices.py | 46 +- scripts/prepare-sidecar.ps1 | 20 + scripts/prepare-sidecar.sh | 8 +- scripts/verify.mjs | 59 + scripts/verify.sh | 19 +- scripts/vr-ci.sh | 9 +- third-party/DAEMON_NOTICES.txt | 2959 +++++-- third-party/DESKTOP_NOTICES.txt | 7328 ++++++++++++++++- third-party/README.md | 8 +- third-party/inventory.json | 588 +- 44 files changed, 11094 insertions(+), 1266 deletions(-) create mode 100644 .gitattributes create mode 100644 .github/workflows/windows.yml create mode 100644 apps/desktop/src-tauri/tauri.windows.conf.json create mode 100644 crates/gravityd/src/paths/windows_hooks.rs create mode 100644 crates/gravityd/src/permissions.rs create mode 100644 crates/gravityd/src/runtime/inbox.rs create mode 100644 crates/gravityd/src/runtime/pty/tests.rs create mode 100644 crates/gravityd/src/service/windows.rs create mode 100644 crates/gravityd/tests/windows_inbox.rs create mode 100644 crates/gravityd/tests/windows_pty.rs create mode 100644 docs/windows.md create mode 100644 scripts/prepare-sidecar.ps1 create mode 100644 scripts/verify.mjs diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..b46a083 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +* text=auto eol=lf +*.png binary +*.ico binary +*.icns binary +*.woff2 binary diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml new file mode 100644 index 0000000..4557777 --- /dev/null +++ b/.github/workflows/windows.yml @@ -0,0 +1,65 @@ +name: Windows + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: windows-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: windows-2022 + env: + CARGO_BUILD_JOBS: 2 + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + - uses: Swatinem/rust-cache@v2 + with: + workspaces: | + . -> target + apps/desktop/src-tauri -> target + - uses: pnpm/action-setup@v4 + with: + version: 11 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + cache-dependency-path: apps/desktop/pnpm-lock.yaml + - name: Install frontend dependencies + run: pnpm --dir apps/desktop install --frozen-lockfile + - name: Check daemon + run: | + cargo fmt --all --check + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + cargo clippy --locked --workspace --all-targets -- -D warnings + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + cargo test --locked --workspace --all-targets + - name: Check frontend + run: pnpm --dir apps/desktop check + - name: Stage daemon sidecar + run: ./scripts/prepare-sidecar.ps1 + - name: Check native shell + run: | + cargo fmt --manifest-path apps/desktop/src-tauri/Cargo.toml --check + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + cargo clippy --locked --manifest-path apps/desktop/src-tauri/Cargo.toml --all-targets -- -D warnings + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + cargo test --locked --manifest-path apps/desktop/src-tauri/Cargo.toml + - name: Build installer + run: pnpm --dir apps/desktop tauri build --bundles nsis + - uses: actions/upload-artifact@v4 + with: + name: gravity-windows-x64 + path: apps/desktop/src-tauri/target/release/bundle/nsis/*-setup.exe + if-no-files-found: error diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ea4d06c..d19e97a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,7 +23,8 @@ to public issues. Security reports belong in the private channel described in Use a current stable Rust toolchain, Node.js 22, and pnpm 11. Building the native macOS app requires Xcode Command Line Tools. Official binaries target Apple -Silicon macOS; other platforms are not currently distributed. Docker is required +Silicon macOS; Windows x64 builds are available as CI artifacts. See +[Windows setup](docs/windows.md) for native prerequisites and PowerShell commands. Docker is required for the visual regression suite. ```sh @@ -55,7 +56,8 @@ pnpm run verify This runs Rust and native-shell checks, desktop and marketing typechecking, linting, tests and builds, the file-length check, and visual regression tests. -It stops on the first failure. Full verification requires macOS and running Docker. +It stops on the first failure. Native-shell checks run on the host platform; +visual verification requires running Docker with Linux containers. Visual baselines must be generated in the pinned CI container; use the repository's visual regression workflow when accepting diff --git a/README.md b/README.md index ccb9fce..a6e0dcd 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ Always-on, multi-bot desktop app with Claude Code as the agent runtime. A Rust daemon (`gravityd`) runs on a Mac mini; the Tauri desktop client attaches from any machine. See the [architecture](docs/architecture.md) for the system design and the [control-plane protocol](docs/protocol.md) for client APIs. +Native Windows x64 builds and local-daemon setup are described in +[Windows build instructions](docs/windows.md). Builds and tests require no Linear, PostHog, or Cloudflare account. Running real bots requires your own Claude Code installation and authentication; the test diff --git a/apps/desktop/src-tauri/src/daemon.rs b/apps/desktop/src-tauri/src/daemon.rs index 3fbac3f..1846d49 100644 --- a/apps/desktop/src-tauri/src/daemon.rs +++ b/apps/desktop/src-tauri/src/daemon.rs @@ -48,31 +48,47 @@ fn sidecar_path_for_exe(exe: &Path) -> Result { let dir = exe .parent() .ok_or_else(|| "the app path has no parent".to_string())?; - Ok(dir.join("gravityd")) + Ok(dir.join(format!("gravityd{}", std::env::consts::EXE_SUFFIX))) } /// Must match `LAUNCHD_LABEL` in `crates/gravityd/src/service.rs`. const LAUNCHD_LABEL: &str = "in.mikolajczuk.gravityd"; -fn user_home() -> Result { - let home = std::env::var("HOME").map_err(|err| format!("HOME is not set: {err}"))?; - Ok(PathBuf::from(home)) +pub(crate) fn user_home() -> Result { + let variable = if cfg!(windows) { "USERPROFILE" } else { "HOME" }; + std::env::var_os(variable) + .map(PathBuf::from) + .ok_or_else(|| format!("{variable} is not set")) } /// Mirrors `ServicePaths::bin_path` and `plist_path` in `crates/gravityd/src/service.rs`. fn managed_daemon_is_installed(home: &Path, user_home: &Path) -> bool { - home.join("bin/gravityd").is_file() - && user_home + home.join(format!("bin/gravityd{}", std::env::consts::EXE_SUFFIX)) + .is_file() + && managed_marker(home, user_home).is_file() +} + +fn managed_marker(home: &Path, user_home: &Path) -> PathBuf { + if cfg!(windows) { + home.join("gravityd-task.xml") + } else { + user_home .join("Library/LaunchAgents") .join(format!("{LAUNCHD_LABEL}.plist")) - .is_file() + } } /// Runs the bundled sidecar's own `service `, the same code path the /// CLI uses. fn run_bundled_service(action: &str, failure: &str) -> Result<(), String> { let sidecar = sidecar_path()?; - let out = std::process::Command::new(&sidecar) + let mut command = std::process::Command::new(&sidecar); + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + command.creation_flags(0x08000000); // CREATE_NO_WINDOW + } + let out = command .args(["service", action]) .output() .map_err(|err| format!("failed to run {}: {err}", sidecar.display()))?; @@ -155,7 +171,7 @@ pub(crate) fn update_local_daemon_if_installed() -> Result<(), String> { const DEFAULT_PORT: u16 = 49777; /// Root of the daemon's state, mirroring `gravityd`'s own resolution. -fn daemon_home() -> Result { +pub(crate) fn daemon_home() -> Result { if let Some(home) = std::env::var_os("GRAVITY_HOME") { return Ok(PathBuf::from(home)); } @@ -255,7 +271,10 @@ mod tests { let exe = Path::new("/Applications/Gravity.app/Contents/MacOS/Gravity"); assert_eq!( sidecar_path_for_exe(exe).expect("sidecar path"), - PathBuf::from("/Applications/Gravity.app/Contents/MacOS/gravityd") + PathBuf::from(format!( + "/Applications/Gravity.app/Contents/MacOS/gravityd{}", + std::env::consts::EXE_SUFFIX + )) ); } @@ -270,10 +289,8 @@ mod tests { let _ = std::fs::remove_dir_all(&root); let home = root.join("gravity"); let user_home = root.join("user"); - let bin = home.join("bin/gravityd"); - let plist = user_home - .join("Library/LaunchAgents") - .join(format!("{LAUNCHD_LABEL}.plist")); + let bin = home.join(format!("bin/gravityd{}", std::env::consts::EXE_SUFFIX)); + let plist = managed_marker(&home, &user_home); std::fs::create_dir_all(bin.parent().expect("binary parent")).expect("binary directory"); std::fs::write(&bin, b"daemon").expect("daemon binary"); assert!(!managed_daemon_is_installed(&home, &user_home)); @@ -314,10 +331,8 @@ mod tests { let root = daemon_test_home("is-managed"); let home = root.join("gravity"); let user_home = root.join("user"); - let bin = home.join("bin/gravityd"); - let plist = user_home - .join("Library/LaunchAgents") - .join(format!("{LAUNCHD_LABEL}.plist")); + let bin = home.join(format!("bin/gravityd{}", std::env::consts::EXE_SUFFIX)); + let plist = managed_marker(&home, &user_home); std::fs::create_dir_all(bin.parent().expect("binary parent")).expect("binary directory"); std::fs::create_dir_all(plist.parent().expect("plist parent")).expect("plist directory"); std::fs::write(&bin, b"daemon").expect("daemon binary"); diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 5956047..e1a6ccf 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -9,8 +9,7 @@ use tauri::{AppHandle, Manager}; /// Reads the daemon client token from `~/.gravity/secrets/client.token`. #[tauri::command] fn read_client_token() -> Result { - let home = std::env::var("HOME").map_err(|err| format!("HOME is not set: {err}"))?; - let path = PathBuf::from(home).join(".gravity/secrets/client.token"); + let path = daemon::daemon_home()?.join("secrets/client.token"); let token = std::fs::read_to_string(&path) .map_err(|err| format!("failed to read {}: {err}", path.display()))?; Ok(token.trim().to_string()) @@ -92,10 +91,7 @@ fn reveal_project(dir_name: String) -> Result<(), String> { if dir_name.is_empty() || dir_name.contains('/') || dir_name.contains('\\') { return Err(format!("not a project directory name: {dir_name}")); } - let home = std::env::var("HOME").map_err(|err| format!("HOME is not set: {err}"))?; - let dir = PathBuf::from(home) - .join(".gravity/projects") - .join(&dir_name); + let dir = daemon::daemon_home()?.join("projects").join(&dir_name); if !dir.is_dir() { return Err(format!("no such project directory: {}", dir.display())); } diff --git a/apps/desktop/src-tauri/tauri.windows.conf.json b/apps/desktop/src-tauri/tauri.windows.conf.json new file mode 100644 index 0000000..68dc3e5 --- /dev/null +++ b/apps/desktop/src-tauri/tauri.windows.conf.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "app": { + "windows": [ + { + "title": "Gravity", + "width": 1280, + "height": 800, + "resizable": true, + "decorations": true + } + ] + }, + "bundle": { + "targets": ["nsis"], + "windows": { + "nsis": { + "installMode": "currentUser" + } + } + } +} diff --git a/apps/marketing/public/THIRD_PARTY_NOTICES.txt b/apps/marketing/public/THIRD_PARTY_NOTICES.txt index 87c08aa..f2a4c33 100644 --- a/apps/marketing/public/THIRD_PARTY_NOTICES.txt +++ b/apps/marketing/public/THIRD_PARTY_NOTICES.txt @@ -1,7 +1,7 @@ GRAVITY MARKETING THIRD-PARTY NOTICES Gravity's MIT license is separate from these upstream terms. -Native inventory target: aarch64-apple-darwin. +Native inventory targets: aarch64-apple-darwin, x86_64-pc-windows-msvc. Runtime dependency closures are conservative; tree-shaken code may be absent. Build/test notices are included conservatively for generated code; their presence does not mean the tools themselves are distributed. diff --git a/crates/gravityd/src/activity.rs b/crates/gravityd/src/activity.rs index 9955c95..e8288b4 100644 --- a/crates/gravityd/src/activity.rs +++ b/crates/gravityd/src/activity.rs @@ -73,7 +73,13 @@ pub(crate) fn transcript_dir(home: &Path, workspace: &Path) -> PathBuf { let mangled: String = workspace .to_string_lossy() .chars() - .map(|c| if c == '/' || c == '.' { '-' } else { c }) + .map(|c| { + if c == '/' || c == '.' || cfg!(windows) && (c == '\\' || c == ':') { + '-' + } else { + c + } + }) .collect(); home.join(".claude").join("projects").join(mangled) } diff --git a/crates/gravityd/src/backup.rs b/crates/gravityd/src/backup.rs index 6ac7a73..35db544 100644 --- a/crates/gravityd/src/backup.rs +++ b/crates/gravityd/src/backup.rs @@ -195,6 +195,8 @@ mod tests { // Existing database blocks restore without --overwrite. assert!(restore(&cfg2, &out, false).is_err()); + // Windows refuses to rename a database while SQLite holds it open. + drop(db2); restore(&cfg2, &out, true).unwrap(); assert!(std::fs::read_dir(home2.path()) .unwrap() diff --git a/crates/gravityd/src/channel.rs b/crates/gravityd/src/channel.rs index de178e8..27024fb 100644 --- a/crates/gravityd/src/channel.rs +++ b/crates/gravityd/src/channel.rs @@ -2,7 +2,9 @@ //! (Claude Code cross-session messaging). Newline-delimited JSON: an optional //! auth line, then a user message. Verified against Claude Code 2.1.251. +#[cfg(unix)] use std::io::Write; +#[cfg(unix)] use std::os::unix::net::UnixStream; use std::path::{Path, PathBuf}; use std::time::Duration; @@ -22,8 +24,10 @@ pub struct MsgSocket { /// tool calls during an active turn, or it starts a new turn when idle — /// never interleaving with terminal input. pub fn send(socket: &MsgSocket, text: &str) -> anyhow::Result<()> { + #[cfg(unix)] let mut stream = UnixStream::connect(&socket.path) .with_context(|| format!("connecting inbox socket {}", socket.path.display()))?; + #[cfg(unix)] stream.set_write_timeout(Some(Duration::from_secs(5)))?; let mut payload = String::new(); @@ -40,13 +44,61 @@ pub fn send(socket: &MsgSocket, text: &str) -> anyhow::Result<()> { ); payload.push('\n'); - stream - .write_all(payload.as_bytes()) - .context("writing to inbox socket")?; - stream.flush()?; + #[cfg(unix)] + { + stream + .write_all(payload.as_bytes()) + .context("writing to inbox socket")?; + stream.flush()?; + } + #[cfg(windows)] + send_pipe(socket, payload)?; Ok(()) } pub fn socket_exists(path: &Path) -> bool { - path.exists() + path.exists() || cfg!(windows) && path.to_string_lossy().starts_with(r"\\.\pipe\") +} + +#[cfg(windows)] +fn send_pipe(socket: &MsgSocket, payload: String) -> anyhow::Result<()> { + anyhow::ensure!( + socket.path.to_string_lossy().starts_with(r"\\.\pipe\"), + "inbox address must be a Windows named pipe" + ); + anyhow::ensure!( + socket.token.as_ref().is_some_and(|t| !t.is_empty()), + "Windows inbox requires a messaging token" + ); + let path = socket.path.clone(); + // A separate I/O runtime bounds writes without nesting Tokio block_on. + std::thread::spawn(move || -> anyhow::Result<()> { + use tokio::io::AsyncWriteExt; + use tokio::net::windows::named_pipe::ClientOptions; + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()?; + runtime.block_on(async { + tokio::time::timeout(Duration::from_secs(5), async { + let mut pipe = loop { + match ClientOptions::new().open(&path) { + Ok(pipe) => break pipe, + Err(error) if error.raw_os_error() == Some(231) => { + tokio::time::sleep(Duration::from_millis(10)).await; + } + Err(error) => return Err(error).context("connecting Windows inbox pipe"), + } + }; + pipe.write_all(payload.as_bytes()) + .await + .context("writing Windows inbox pipe")?; + pipe.flush().await?; + Ok(()) + }) + .await + .context("Windows inbox delivery timed out")? + }) + }) + .join() + .map_err(|_| anyhow::anyhow!("Windows inbox worker panicked"))? } diff --git a/crates/gravityd/src/config.rs b/crates/gravityd/src/config.rs index ec22822..fa4a1e8 100644 --- a/crates/gravityd/src/config.rs +++ b/crates/gravityd/src/config.rs @@ -196,9 +196,9 @@ fn resolve_home(gravity_home: Option, user_home: PathBuf) -> PathBuf { } fn dirs_home() -> PathBuf { - std::env::var_os("HOME") + std::env::var_os(if cfg!(windows) { "USERPROFILE" } else { "HOME" }) .map(PathBuf::from) - .unwrap_or_else(|| PathBuf::from("/tmp")) + .unwrap_or_else(std::env::temp_dir) } impl Config { diff --git a/crates/gravityd/src/lib.rs b/crates/gravityd/src/lib.rs index 4a35d64..1087461 100644 --- a/crates/gravityd/src/lib.rs +++ b/crates/gravityd/src/lib.rs @@ -18,12 +18,17 @@ pub mod messaging; pub mod model; pub mod overrides; pub mod paths; +mod permissions; pub mod projectmgmt; pub mod routine_validation; pub mod runtime; pub mod scheduler; pub mod secrets; pub mod server; +#[cfg(not(windows))] +pub mod service; +#[cfg(windows)] +#[path = "service/windows.rs"] pub mod service; pub mod supervisor; pub mod terminal; diff --git a/crates/gravityd/src/main.rs b/crates/gravityd/src/main.rs index adad5e3..e6e803a 100644 --- a/crates/gravityd/src/main.rs +++ b/crates/gravityd/src/main.rs @@ -74,7 +74,7 @@ async fn main() -> anyhow::Result<()> { println!( "gravityd installed to {} and running ({})", paths.bin_path().display(), - gravityd::service::LAUNCHD_LABEL + gravityd::service::SERVICE_LABEL ); println!("Logs: {}", paths.log_dir().display()); } @@ -92,7 +92,7 @@ async fn main() -> anyhow::Result<()> { paths.plist_path().display() ); gravityd::service::reload(&paths)?; - println!("gravityd restarted ({})", gravityd::service::LAUNCHD_LABEL); + println!("gravityd restarted ({})", gravityd::service::SERVICE_LABEL); } Some("status") => { if !gravityd::service::status(&paths, cfg.port) { diff --git a/crates/gravityd/src/paths.rs b/crates/gravityd/src/paths.rs index edb2b10..fcec437 100644 --- a/crates/gravityd/src/paths.rs +++ b/crates/gravityd/src/paths.rs @@ -19,6 +19,7 @@ use std::fs::{self, OpenOptions}; use std::io::Write; +#[cfg(unix)] use std::os::unix::fs::OpenOptionsExt; use std::path::{Path, PathBuf}; @@ -31,6 +32,8 @@ mod prompt; #[cfg(test)] mod tests; mod trust; +#[cfg(windows)] +mod windows_hooks; pub use prompt::system_md; pub use trust::trust_workspace; @@ -234,7 +237,10 @@ pub fn write_hook_settings( ) -> anyhow::Result<()> { let dir = workspace.join(".claude"); fs::create_dir_all(&dir)?; + #[cfg(unix)] let settings = hook_settings(daemon_port, bot_token_env); + #[cfg(windows)] + let settings = windows_hooks::settings(workspace, daemon_port, bot_token_env)?; atomic_write_json(&dir.join("settings.json"), &settings) } @@ -258,6 +264,7 @@ pub fn artifacts_allow_rules(artifacts_dir: &Path) -> Vec { /// events to the daemon; failures are swallowed (`|| true`) so a daemon /// hiccup never blocks the session, and hook failure is never interpreted as /// approval or denial. +#[cfg(unix)] fn hook_settings(daemon_port: u16, bot_token_env: &str) -> serde_json::Value { let hook_cmd = |event: &str| { serde_json::json!([{ @@ -347,12 +354,14 @@ pub(super) fn atomic_write_private_json( ) -> anyhow::Result<()> { let tmp = path.with_extension(format!("json.tmp-{}", uuid::Uuid::new_v4())); let result = (|| -> anyhow::Result<()> { - let mut file = OpenOptions::new() - .write(true) - .create_new(true) - .mode(0o600) + let mut options = OpenOptions::new(); + options.write(true).create_new(true); + #[cfg(unix)] + options.mode(0o600); + let mut file = options .open(&tmp) .with_context(|| format!("creating {}", tmp.display()))?; + crate::permissions::private(&tmp, false)?; serde_json::to_writer_pretty(&mut file, value) .with_context(|| format!("writing {}", tmp.display()))?; file.write_all(b"\n") diff --git a/crates/gravityd/src/paths/trust.rs b/crates/gravityd/src/paths/trust.rs index 8e319ea..d6b85a6 100644 --- a/crates/gravityd/src/paths/trust.rs +++ b/crates/gravityd/src/paths/trust.rs @@ -32,15 +32,13 @@ pub fn trust_workspace(user_home: &Path, workspace: &Path) -> anyhow::Result<()> let workspace = workspace .canonicalize() .with_context(|| format!("resolving workspace {}", workspace.display()))?; - let workspace_key = workspace - .to_str() - .context("canonical workspace path is not valid UTF-8")?; + let workspace_key = project_key(&workspace)?; let config_path = user_home.join(".claude.json"); // The common case is a workspace that is already trusted. Read it unlocked: // taking the shared lock for a no-op would make a live session's own save // fail for nothing. - if is_trusted(&read_claude_config(&config_path)?, workspace_key) { + if is_trusted(&read_claude_config(&config_path)?, &workspace_key) { return Ok(()); } @@ -49,7 +47,7 @@ pub fn trust_workspace(user_home: &Path, workspace: &Path) -> anyhow::Result<()> // happen under the lock. let _lock = ClaudeConfigLock::acquire(&config_path)?; let mut config = read_claude_config(&config_path)?; - if is_trusted(&config, workspace_key) { + if is_trusted(&config, &workspace_key) { return Ok(()); } let root = config @@ -88,6 +86,20 @@ fn is_trusted(config: &serde_json::Value, workspace_key: &str) -> bool { config["projects"][workspace_key]["hasTrustDialogAccepted"] == serde_json::Value::Bool(true) } +fn project_key(path: &Path) -> anyhow::Result { + let path = path.to_str().context("workspace path is not valid UTF-8")?; + // Rust canonicalization returns verbatim paths; Node's cwd does not. + #[cfg(windows)] + { + if let Some(rest) = path.strip_prefix(r"\\?\UNC\") { + return Ok(format!(r"\\{rest}")); + } + Ok(path.strip_prefix(r"\\?\").unwrap_or(path).to_string()) + } + #[cfg(not(windows))] + Ok(path.to_string()) +} + /// The advisory lock Claude Code takes before saving `~/.claude.json`: a /// `.lock` *directory*, where `mkdir` is the atomic acquire and the /// mtime is refreshed for as long as it is held. Holding the same one is what @@ -150,6 +162,7 @@ fn is_stale_lock(dir: &Path) -> bool { #[cfg(test)] mod tests { use super::*; + #[cfg(unix)] use std::os::unix::fs::PermissionsExt; /// A temp home with a workspace and a `.claude.json` holding `config`. @@ -185,7 +198,7 @@ mod tests { serde_json::to_vec_pretty(&serde_json::json!({ "theme": "dark", "projects": { - canonical_workspace.display().to_string(): { + project_key(&canonical_workspace).expect("project key"): { "allowedTools": ["Read"], "hasTrustDialogAccepted": false }, @@ -204,11 +217,13 @@ mod tests { .expect("parse config"); assert_eq!(config["theme"], "dark"); assert_eq!( - config["projects"][canonical_workspace.display().to_string()]["allowedTools"][0], + config["projects"][project_key(&canonical_workspace).expect("project key")] + ["allowedTools"][0], "Read" ); assert_eq!( - config["projects"][canonical_workspace.display().to_string()]["hasTrustDialogAccepted"], + config["projects"][project_key(&canonical_workspace).expect("project key")] + ["hasTrustDialogAccepted"], true ); assert_eq!( @@ -242,11 +257,22 @@ mod tests { let (tmp, workspace, config_path) = trust_fixture(r#"{"theme":"dark"}"#); let lock = config_lock_path(&config_path); fs::create_dir(&lock).expect("hold lock"); + let mut options = fs::OpenOptions::new(); + options.write(true); + #[cfg(windows)] + { + use std::os::windows::fs::OpenOptionsExt; + options.custom_flags(0x02000000); // FILE_FLAG_BACKUP_SEMANTICS + } + #[cfg(unix)] let held = fs::File::open(&lock).expect("open lock"); + #[cfg(windows)] + let held = options.open(&lock).expect("open lock"); held.set_times( fs::FileTimes::new().set_modified(SystemTime::now() - Duration::from_secs(3600)), ) .expect("age the lock"); + drop(held); trust_workspace(tmp.path(), &workspace).expect("trust"); @@ -255,11 +281,7 @@ mod tests { assert_eq!(config["theme"], "dark"); assert!(is_trusted( &config, - workspace - .canonicalize() - .expect("canonical") - .to_str() - .unwrap() + &project_key(&workspace.canonicalize().expect("canonical")).expect("project key") )); } @@ -277,7 +299,7 @@ mod tests { &config_path, serde_json::to_vec_pretty(&serde_json::json!({ "projects": { - canonical.display().to_string(): { "hasTrustDialogAccepted": true } + project_key(&canonical).expect("project key"): { "hasTrustDialogAccepted": true } } })) .expect("json"), @@ -302,9 +324,11 @@ mod tests { serde_json::from_slice(&fs::read(&config_path).expect("read config")) .expect("parse config"); assert_eq!( - config["projects"][canonical_workspace.display().to_string()]["hasTrustDialogAccepted"], + config["projects"][project_key(&canonical_workspace).expect("project key")] + ["hasTrustDialogAccepted"], true ); + #[cfg(unix)] assert_eq!( fs::metadata(config_path) .expect("metadata") diff --git a/crates/gravityd/src/paths/windows_hooks.rs b/crates/gravityd/src/paths/windows_hooks.rs new file mode 100644 index 0000000..a68b7e1 --- /dev/null +++ b/crates/gravityd/src/paths/windows_hooks.rs @@ -0,0 +1,47 @@ +//! Serialize hook payloads with PowerShell so Windows pipe paths remain valid JSON. +use std::path::Path; + +pub fn settings(workspace: &Path, port: u16, token_env: &str) -> anyhow::Result { + let script = workspace.join(".claude/gravity-hook.ps1"); + let body = format!( + r#"param([string]$Event) +$ErrorActionPreference = 'Stop' +try {{ + $payload = @{{ event = $Event }} + if ($Event -eq 'SessionStart') {{ + $payload.socket = $env:CLAUDE_CODE_MESSAGING_SOCKET + $payload.msg_token = $env:CLAUDE_CODE_MESSAGING_TOKEN + }} + if ($Event -eq 'Notification' -or $Event -eq 'Stop') {{ + $inputJson = [Console]::In.ReadToEnd() | ConvertFrom-Json + $payload.message = $inputJson.message + $payload.transcript_path = $inputJson.transcript_path + }} + $json = $payload | ConvertTo-Json -Compress + Invoke-RestMethod -Uri 'http://127.0.0.1:{port}/hook' -Method Post -TimeoutSec 3 -ContentType 'application/json; charset=utf-8' -Headers @{{ Authorization = "Bearer $env:{token_env}" }} -Body ([Text.Encoding]::UTF8.GetBytes($json)) | Out-Null +}} catch {{ }} +exit 0 +"# + ); + std::fs::write(&script, body)?; + let events = [ + "SessionStart", + "UserPromptSubmit", + "PostToolUse", + "Stop", + "Notification", + "SessionEnd", + ]; + let mut hooks = serde_json::Map::new(); + for event in events { + hooks.insert(event.to_string(), serde_json::json!([{ "hooks": [{ + "type": "command", + "command": format!("powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File \"{}\" {event}", script.display()) + }] }])); + } + Ok(serde_json::json!({ + "crossSessionInbound": "accept", + "permissions": { "allow": [], "deny": ["Read(../**)", "Read(~/.gravity/secrets/**)", "Bash(rm -rf /*)"] }, + "hooks": hooks + })) +} diff --git a/crates/gravityd/src/permissions.rs b/crates/gravityd/src/permissions.rs new file mode 100644 index 0000000..0dd0342 --- /dev/null +++ b/crates/gravityd/src/permissions.rs @@ -0,0 +1,53 @@ +//! Private token/config files: Unix modes or a current-user Windows ACL. +use std::path::Path; + +#[cfg(unix)] +pub fn private(path: &Path, directory: bool) -> anyhow::Result<()> { + use std::os::unix::fs::PermissionsExt; + let mode = if directory { 0o700 } else { 0o600 }; + std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))?; + Ok(()) +} + +#[cfg(windows)] +pub fn user_sid() -> anyhow::Result { + use anyhow::Context; + let out = std::process::Command::new("whoami.exe") + .args(["/user", "/fo", "csv", "/nh"]) + .output() + .context("identifying the Windows user")?; + anyhow::ensure!(out.status.success(), "cannot identify Windows user"); + let text = String::from_utf8(out.stdout)?; + let sid = text + .trim() + .rsplit(',') + .next() + .context("missing Windows SID")? + .trim_matches('"'); + anyhow::ensure!( + sid.starts_with("S-1-") + && sid + .chars() + .all(|c| c.is_ascii_digit() || c == 'S' || c == '-'), + "invalid Windows SID" + ); + Ok(sid.to_string()) +} + +#[cfg(windows)] +pub fn private(path: &Path, directory: bool) -> anyhow::Result<()> { + use anyhow::Context; + let sid = user_sid()?; + let grant = format!("*{sid}:{}F", if directory { "(OI)(CI)" } else { "" }); + let out = std::process::Command::new("icacls.exe") + .arg(path) + .args(["/inheritance:r", "/grant:r", &grant]) + .output() + .context("restricting private file permissions")?; + anyhow::ensure!( + out.status.success(), + "cannot restrict private file permissions: {}", + String::from_utf8_lossy(&out.stderr) + ); + Ok(()) +} diff --git a/crates/gravityd/src/runtime/double.rs b/crates/gravityd/src/runtime/double.rs index 3937187..ab752d1 100644 --- a/crates/gravityd/src/runtime/double.rs +++ b/crates/gravityd/src/runtime/double.rs @@ -3,7 +3,9 @@ //! cross-session messaging) and echoes delivered messages and typed input to //! its terminal output, so the full channel-delivery path is exercised. +#[cfg(unix)] use std::io::{BufRead, BufReader}; +#[cfg(unix)] use std::os::unix::net::UnixListener; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; @@ -11,6 +13,7 @@ use std::sync::Arc; use tokio::sync::mpsc; use super::{BotSpec, Capabilities, RuntimeAdapter, RuntimeSession, SessionEvent, StartedSession}; +#[cfg(unix)] use crate::channel::MsgSocket; /// Ctrl-D: ends the double session as an unsolicited exit. @@ -51,16 +54,25 @@ impl RuntimeAdapter for DoubleAdapter { // Real inbox socket speaking the cross-session wire protocol. Unix // socket paths are capped at ~104 bytes on macOS, so keep it short: // /tmp + pid + a bot-id prefix. + #[cfg(unix)] let sock_dir = std::path::PathBuf::from(format!("/tmp/cbd-{}", std::process::id())); + #[cfg(unix)] std::fs::create_dir_all(&sock_dir)?; + #[cfg(unix)] let short_id: String = spec.bot_id.chars().take(8).collect(); + #[cfg(unix)] let sock_path = sock_dir.join(format!("{short_id}.sock")); + #[cfg(unix)] let _ = std::fs::remove_file(&sock_path); + #[cfg(unix)] let listener = UnixListener::bind(&sock_path)?; let alive = Arc::new(AtomicBool::new(true)); + #[cfg(unix)] let inbox_tx = tx.clone(); + #[cfg(unix)] let inbox_alive = alive.clone(); + #[cfg(unix)] std::thread::spawn(move || { for conn in listener.incoming() { if !inbox_alive.load(Ordering::SeqCst) { @@ -89,17 +101,25 @@ impl RuntimeAdapter for DoubleAdapter { } }); + #[cfg(unix)] + let socket = MsgSocket { + path: sock_path, + token: None, + }; + #[cfg(windows)] + let (socket, inbox_task) = super::inbox::start(tx.clone())?; + Ok(StartedSession { session: Box::new(DoubleSession { tx, alive, - sock_path: sock_path.clone(), + #[cfg(unix)] + sock_path: socket.path.clone(), + #[cfg(windows)] + inbox_task, }), events: rx, - msg_socket: Some(MsgSocket { - path: sock_path, - token: None, - }), + msg_socket: Some(socket), }) } @@ -111,7 +131,10 @@ impl RuntimeAdapter for DoubleAdapter { struct DoubleSession { tx: mpsc::UnboundedSender, alive: Arc, + #[cfg(unix)] sock_path: std::path::PathBuf, + #[cfg(windows)] + inbox_task: tokio::task::AbortHandle, } impl RuntimeSession for DoubleSession { @@ -153,7 +176,10 @@ impl RuntimeSession for DoubleSession { impl DoubleSession { fn end(&mut self, code: Option) { self.alive.store(false, Ordering::SeqCst); + #[cfg(unix)] let _ = std::fs::remove_file(&self.sock_path); + #[cfg(windows)] + self.inbox_task.abort(); let _ = self.tx.send(SessionEvent::Exited { code }); } } diff --git a/crates/gravityd/src/runtime/inbox.rs b/crates/gravityd/src/runtime/inbox.rs new file mode 100644 index 0000000..3e725c6 --- /dev/null +++ b/crates/gravityd/src/runtime/inbox.rs @@ -0,0 +1,89 @@ +//! Authenticated Windows inbox for the deterministic runtime. +use anyhow::Context; +use tokio::io::{AsyncBufReadExt, BufReader}; +use tokio::net::windows::named_pipe::ServerOptions; +use tokio::sync::mpsc; +use tokio::task::AbortHandle; + +use super::SessionEvent; +use crate::channel::MsgSocket; + +pub fn start(tx: mpsc::UnboundedSender) -> anyhow::Result<(MsgSocket, AbortHandle)> { + let (ready_tx, ready_rx) = std::sync::mpsc::sync_channel(1); + // Deliveries are synchronous: keep the receiver off the daemon's Tokio + // thread so a full pipe cannot block the receiver that would drain it. + std::thread::spawn(move || { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build(); + match runtime { + Ok(runtime) => runtime.block_on(async { + match spawn_inbox(tx) { + Ok((socket, task)) => { + if ready_tx.send(Ok((socket, task.abort_handle()))).is_err() { + task.abort(); + } + let _ = task.await; + } + Err(error) => { + let _ = ready_tx.send(Err(error)); + } + } + }), + Err(error) => { + let _ = ready_tx.send(Err(error.into())); + } + } + }); + ready_rx.recv().context("Windows inbox worker stopped")? +} + +fn spawn_inbox( + tx: mpsc::UnboundedSender, +) -> anyhow::Result<(MsgSocket, tokio::task::JoinHandle<()>)> { + let path = format!(r"\\.\pipe\gravity-double-{}", uuid::Uuid::new_v4()); + let token = uuid::Uuid::new_v4().to_string(); + let server = ServerOptions::new() + .first_pipe_instance(true) + .create(&path)?; + let socket = MsgSocket { + path: path.clone().into(), + token: Some(token.clone()), + }; + let task = tokio::spawn(async move { + let mut server = server; + loop { + if server.connect().await.is_err() { + break; + } + let next = match ServerOptions::new().create(&path) { + Ok(next) => next, + Err(_) => break, + }; + let mut lines = BufReader::new(server).lines(); + let read = async { + let auth = lines.next_line().await?.context("missing auth")?; + let auth: serde_json::Value = serde_json::from_str(&auth)?; + anyhow::ensure!( + auth["type"] == "auth" && auth["token"].as_str() == Some(&token), + "invalid auth" + ); + while let Some(line) = lines.next_line().await? { + let value: serde_json::Value = serde_json::from_str(&line)?; + if value["type"] == "user" { + if let Some(content) = + value.pointer("/message/content").and_then(|c| c.as_str()) + { + let _ = tx + .send(SessionEvent::Output(format!("{content}\r\n").into_bytes())); + } + } + } + Ok::<(), anyhow::Error>(()) + }; + let _ = tokio::time::timeout(std::time::Duration::from_secs(5), read).await; + server = next; + } + }); + Ok((socket, task)) +} diff --git a/crates/gravityd/src/runtime/mod.rs b/crates/gravityd/src/runtime/mod.rs index 3ebf995..0beddcc 100644 --- a/crates/gravityd/src/runtime/mod.rs +++ b/crates/gravityd/src/runtime/mod.rs @@ -5,6 +5,8 @@ //! inbox socket (see `channel`). The PTY carries only terminal I/O. pub mod double; +#[cfg(windows)] +mod inbox; pub mod pty; use std::path::PathBuf; diff --git a/crates/gravityd/src/runtime/pty.rs b/crates/gravityd/src/runtime/pty.rs index 783de09..7b5020f 100644 --- a/crates/gravityd/src/runtime/pty.rs +++ b/crates/gravityd/src/runtime/pty.rs @@ -3,6 +3,7 @@ //! go through the session's inbox socket, reported by the SessionStart hook. use std::io::{Read, Write}; +#[cfg(unix)] use std::os::fd::RawFd; use std::process::Command as StdCommand; use std::time::{Duration, Instant}; @@ -68,21 +69,44 @@ impl RuntimeAdapter for PtyAdapter { let mut reader = pair.master.try_clone_reader().context("clone pty reader")?; let writer = pair.master.take_writer().context("take pty writer")?; + #[cfg(unix)] let master_fd = pair.master.as_raw_fd(); let (tx, rx) = mpsc::unbounded_channel(); let killer = child.clone_killer(); + // ConPTY keeps its read pipe open until the master is dropped. Waiting + // for reader EOF first would hide exits forever, preventing restarts. + #[cfg(windows)] + { + let exit_tx = tx.clone(); + std::thread::spawn(move || { + let mut child = child; + let code = child.wait().ok().map(|s| s.exit_code() as i32); + // Allow the console host to flush its final screen update. + std::thread::sleep(Duration::from_millis(50)); + let _ = exit_tx.send(SessionEvent::Exited { code }); + }); + } + // Blocking reader thread; ends when the child exits and the pty EOFs. let out_tx = tx.clone(); std::thread::spawn(move || { + #[cfg(unix)] let mut child = child; + #[cfg(unix)] let more_soon = |timeout| master_fd.is_some_and(|fd| readable_within(fd, timeout)); + // ConPTY exposes a blocking pipe rather than a pollable Unix fd. + #[cfg(windows)] + let more_soon = |_timeout| false; pump(&mut reader, more_soon, |chunk| { out_tx.send(SessionEvent::Output(chunk)).is_ok() }); - let code = child.wait().ok().map(|s| s.exit_code() as i32); - let _ = out_tx.send(SessionEvent::Exited { code }); + #[cfg(unix)] + { + let code = child.wait().ok().map(|s| s.exit_code() as i32); + let _ = out_tx.send(SessionEvent::Exited { code }); + } }); Ok(StartedSession { @@ -114,6 +138,7 @@ impl RuntimeAdapter for PtyAdapter { } /// Whether `fd` has output to read within `timeout`. +#[cfg(unix)] fn readable_within(fd: RawFd, timeout: Duration) -> bool { let mut pfd = libc::pollfd { fd, @@ -229,159 +254,4 @@ impl RuntimeSession for PtySession { } #[cfg(test)] -mod tests { - use super::{pump, utf8_prefix_len, COALESCE_WINDOW, MAX_FRAME_BYTES}; - use std::collections::VecDeque; - use std::io::Read; - - /// A reader that returns each queued write as its own `read`, the way a - /// pty hands over one small write at a time. - struct Writes(VecDeque>); - - impl Read for Writes { - fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - let Some(mut write) = self.0.pop_front() else { - return Ok(0); - }; - // A write larger than the caller's buffer comes back in pieces, - // exactly as a pty read would return it. - let n = write.len().min(buf.len()); - let rest = write.split_off(n); - if !rest.is_empty() { - self.0.push_front(rest); - } - buf[..n].copy_from_slice(&write); - Ok(n) - } - } - - fn frames(writes: &[&[u8]], mut more_soon: impl FnMut() -> bool) -> Vec> { - let mut reader = Writes(writes.iter().map(|w| w.to_vec()).collect()); - let mut out = Vec::new(); - pump( - &mut reader, - |_| more_soon(), - |chunk| { - out.push(chunk); - true - }, - ); - out - } - - #[test] - fn merges_a_burst_of_small_writes_into_one_frame() { - let out = frames(&[b"\x1b[2K", b"prompt", b" > ", b"\x1b[1A"], || true); - assert_eq!(out, vec![b"\x1b[2Kprompt > \x1b[1A".to_vec()]); - } - - #[test] - fn emits_separate_frames_when_output_pauses() { - let out = frames(&[b"first", b"second"], || false); - assert_eq!(out, vec![b"first".to_vec(), b"second".to_vec()]); - } - - #[test] - fn flushes_continuous_output_when_the_batch_deadline_expires() { - let out = frames(&[b"a", b"b", b"c"], || { - // More data keeps arriving, but waiting for it uses up the batch's - // time budget. It must not start another full wait for each read. - std::thread::sleep(COALESCE_WINDOW); - true - }); - assert!(out.len() >= 2, "continuous output must not wait for EOF"); - assert_eq!(out.concat(), b"abc"); - } - - #[test] - fn caps_a_merged_frame() { - let big = vec![b'x'; MAX_FRAME_BYTES - 1]; - let out = frames(&[&big, b"yy", b"z"], || true); - assert_eq!(out.len(), 2, "the cap ends the frame before it overflows"); - assert_eq!(out[0].len(), MAX_FRAME_BYTES + 1); - assert_eq!(out[1], b"z"); - } - - #[test] - fn holds_an_incomplete_character_for_the_next_read() { - let glyph = "╭".as_bytes(); - let out = frames(&[b"a", &glyph[..1], &glyph[1..]], || false); - assert_eq!(out, vec![b"a".to_vec(), glyph.to_vec()]); - } - - #[test] - fn stops_once_the_consumer_is_gone() { - let mut reader = Writes([b"a".to_vec(), b"b".to_vec()].into()); - let mut seen = 0; - pump( - &mut reader, - |_| false, - |_| { - seen += 1; - false - }, - ); - assert_eq!(seen, 1); - } - - /// The real thing: a shell writing one tiny escape sequence per syscall, - /// the way a TUI repaints, must reach the daemon as a few frames, not one - /// frame per write. Timing-dependent by nature, so the bound is loose. - #[tokio::test] - async fn a_real_pty_burst_is_merged_into_few_frames() { - use super::{PtyAdapter, RuntimeAdapter}; - use crate::runtime::{BotSpec, SessionEvent}; - - const WRITES: usize = 400; - let script = format!( - "i=0; while [ $i -lt {WRITES} ]; do printf '\\033[2K\\033[1A%04d' $i; i=$((i+1)); done" - ); - let spec = BotSpec { - bot_id: "pty-test".into(), - bot_name: "pty".into(), - workspace: std::env::temp_dir(), - claude_bin: "/bin/sh".into(), - claude_args: vec!["-c".into(), script], - env: Vec::new(), - cols: 80, - rows: 24, - }; - let mut started = PtyAdapter.start(&spec).expect("spawn sh in a pty"); - let mut frames = 0usize; - let mut bytes = 0usize; - while let Some(event) = started.events.recv().await { - match event { - SessionEvent::Output(data) => { - frames += 1; - bytes += data.len(); - } - SessionEvent::Exited { .. } => break, - } - } - // Each write is two 4-byte escape sequences plus 4 digits. - assert_eq!(bytes, WRITES * 12, "every byte the shell wrote arrived"); - assert!( - frames * 4 < WRITES, - "{WRITES} writes arrived as {frames} frames; expected far fewer" - ); - eprintln!("real pty: {WRITES} writes -> {frames} frames ({bytes} bytes)"); - } - - #[test] - fn holds_back_incomplete_trailing_sequences() { - let full = "ab╭─".as_bytes(); // '╭' and '─' are 3 bytes each - assert_eq!(utf8_prefix_len(full), full.len()); - assert_eq!(utf8_prefix_len(&full[..full.len() - 1]), full.len() - 3); - assert_eq!(utf8_prefix_len(&full[..full.len() - 2]), full.len() - 3); - assert_eq!(utf8_prefix_len(&full[..full.len() - 3]), full.len() - 3); - assert_eq!(utf8_prefix_len(b""), 0); - assert_eq!(utf8_prefix_len(b"plain ascii"), 11); - } - - #[test] - fn keeps_complete_four_byte_sequences() { - let full = "x\u{1F600}".as_bytes(); - assert_eq!(utf8_prefix_len(full), full.len()); - assert_eq!(utf8_prefix_len(&full[..full.len() - 1]), 1); - } -} +mod tests; diff --git a/crates/gravityd/src/runtime/pty/tests.rs b/crates/gravityd/src/runtime/pty/tests.rs new file mode 100644 index 0000000..6976d19 --- /dev/null +++ b/crates/gravityd/src/runtime/pty/tests.rs @@ -0,0 +1,158 @@ +use super::{pump, utf8_prefix_len, COALESCE_WINDOW, MAX_FRAME_BYTES}; +use std::collections::VecDeque; +use std::io::Read; + +/// A reader that returns each queued write as its own `read`, the way a +/// pty hands over one small write at a time. +struct Writes(VecDeque>); + +impl Read for Writes { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let Some(mut write) = self.0.pop_front() else { + return Ok(0); + }; + // A write larger than the caller's buffer comes back in pieces, + // exactly as a pty read would return it. + let n = write.len().min(buf.len()); + let rest = write.split_off(n); + if !rest.is_empty() { + self.0.push_front(rest); + } + buf[..n].copy_from_slice(&write); + Ok(n) + } +} + +fn frames(writes: &[&[u8]], mut more_soon: impl FnMut() -> bool) -> Vec> { + let mut reader = Writes(writes.iter().map(|w| w.to_vec()).collect()); + let mut out = Vec::new(); + pump( + &mut reader, + |_| more_soon(), + |chunk| { + out.push(chunk); + true + }, + ); + out +} + +#[test] +fn merges_a_burst_of_small_writes_into_one_frame() { + let out = frames(&[b"\x1b[2K", b"prompt", b" > ", b"\x1b[1A"], || true); + assert_eq!(out, vec![b"\x1b[2Kprompt > \x1b[1A".to_vec()]); +} + +#[test] +fn emits_separate_frames_when_output_pauses() { + let out = frames(&[b"first", b"second"], || false); + assert_eq!(out, vec![b"first".to_vec(), b"second".to_vec()]); +} + +#[test] +fn flushes_continuous_output_when_the_batch_deadline_expires() { + let out = frames(&[b"a", b"b", b"c"], || { + // More data keeps arriving, but waiting for it uses up the batch's + // time budget. It must not start another full wait for each read. + std::thread::sleep(COALESCE_WINDOW); + true + }); + assert!(out.len() >= 2, "continuous output must not wait for EOF"); + assert_eq!(out.concat(), b"abc"); +} + +#[test] +fn caps_a_merged_frame() { + let big = vec![b'x'; MAX_FRAME_BYTES - 1]; + let out = frames(&[&big, b"yy", b"z"], || true); + // The time limit can split a batch sooner on a busy machine. + assert!(out.len() >= 2, "the byte cap must split this output"); + assert!(out + .iter() + .all(|frame| frame.len() <= MAX_FRAME_BYTES + 8191)); + assert_eq!(out.concat(), [big, b"yyz".to_vec()].concat()); +} + +#[test] +fn holds_an_incomplete_character_for_the_next_read() { + let glyph = "╭".as_bytes(); + let out = frames(&[b"a", &glyph[..1], &glyph[1..]], || false); + assert_eq!(out, vec![b"a".to_vec(), glyph.to_vec()]); +} + +#[test] +fn stops_once_the_consumer_is_gone() { + let mut reader = Writes([b"a".to_vec(), b"b".to_vec()].into()); + let mut seen = 0; + pump( + &mut reader, + |_| false, + |_| { + seen += 1; + false + }, + ); + assert_eq!(seen, 1); +} + +/// The real thing: a shell writing one tiny escape sequence per syscall, +/// the way a TUI repaints, must reach the daemon as a few frames, not one +/// frame per write. Timing-dependent by nature, so the bound is loose. +#[cfg(unix)] +#[tokio::test] +async fn a_real_pty_burst_is_merged_into_few_frames() { + use super::{PtyAdapter, RuntimeAdapter}; + use crate::runtime::{BotSpec, SessionEvent}; + + const WRITES: usize = 400; + let script = format!( + "i=0; while [ $i -lt {WRITES} ]; do printf '\\033[2K\\033[1A%04d' $i; i=$((i+1)); done" + ); + let spec = BotSpec { + bot_id: "pty-test".into(), + bot_name: "pty".into(), + workspace: std::env::temp_dir(), + claude_bin: "/bin/sh".into(), + claude_args: vec!["-c".into(), script], + env: Vec::new(), + cols: 80, + rows: 24, + }; + let mut started = PtyAdapter.start(&spec).expect("spawn sh in a pty"); + let mut frames = 0usize; + let mut bytes = 0usize; + while let Some(event) = started.events.recv().await { + match event { + SessionEvent::Output(data) => { + frames += 1; + bytes += data.len(); + } + SessionEvent::Exited { .. } => break, + } + } + // Each write is two 4-byte escape sequences plus 4 digits. + assert_eq!(bytes, WRITES * 12, "every byte the shell wrote arrived"); + assert!( + frames * 4 < WRITES, + "{WRITES} writes arrived as {frames} frames; expected far fewer" + ); + eprintln!("real pty: {WRITES} writes -> {frames} frames ({bytes} bytes)"); +} + +#[test] +fn holds_back_incomplete_trailing_sequences() { + let full = "ab╭─".as_bytes(); // '╭' and '─' are 3 bytes each + assert_eq!(utf8_prefix_len(full), full.len()); + assert_eq!(utf8_prefix_len(&full[..full.len() - 1]), full.len() - 3); + assert_eq!(utf8_prefix_len(&full[..full.len() - 2]), full.len() - 3); + assert_eq!(utf8_prefix_len(&full[..full.len() - 3]), full.len() - 3); + assert_eq!(utf8_prefix_len(b""), 0); + assert_eq!(utf8_prefix_len(b"plain ascii"), 11); +} + +#[test] +fn keeps_complete_four_byte_sequences() { + let full = "x\u{1F600}".as_bytes(); + assert_eq!(utf8_prefix_len(full), full.len()); + assert_eq!(utf8_prefix_len(&full[..full.len() - 1]), 1); +} diff --git a/crates/gravityd/src/secrets.rs b/crates/gravityd/src/secrets.rs index d005536..48fbb58 100644 --- a/crates/gravityd/src/secrets.rs +++ b/crates/gravityd/src/secrets.rs @@ -4,7 +4,6 @@ use std::collections::HashMap; use std::fs; -use std::os::unix::fs::PermissionsExt; use std::path::{Path, PathBuf}; use std::sync::Mutex; @@ -28,14 +27,14 @@ fn random_token() -> String { fn write_secret(path: &Path, value: &str) -> anyhow::Result<()> { fs::write(path, value)?; - fs::set_permissions(path, fs::Permissions::from_mode(0o600))?; + crate::permissions::private(path, false)?; Ok(()) } impl Secrets { pub fn open(dir: &Path) -> anyhow::Result { fs::create_dir_all(dir)?; - fs::set_permissions(dir, fs::Permissions::from_mode(0o700))?; + crate::permissions::private(dir, true)?; let client_path = dir.join("client.token"); let client_token = if client_path.exists() { diff --git a/crates/gravityd/src/server/port.rs b/crates/gravityd/src/server/port.rs index cd8c7f2..7b6b199 100644 --- a/crates/gravityd/src/server/port.rs +++ b/crates/gravityd/src/server/port.rs @@ -17,6 +17,12 @@ const PORT_GRACE_INTERVAL: Duration = Duration::from_millis(250); /// How often a daemon on a fallback port checks whether it can go home. pub(super) const PORT_RECLAIM_INTERVAL: Duration = Duration::from_secs(30); +fn port_unavailable(error: &std::io::Error) -> bool { + error.kind() == std::io::ErrorKind::AddrInUse + // Hyper-V can reserve the preferred loopback port without a listener. + || (cfg!(windows) && error.raw_os_error() == Some(10013)) +} + /// What a collision on the configured port means for this launch. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PortPolicy { @@ -111,7 +117,7 @@ async fn bind_with_grace( Ok(bound) => return Ok(bound), Err(error) => error, }; - if policy == PortPolicy::Strict || error.kind() != std::io::ErrorKind::AddrInUse { + if policy == PortPolicy::Strict || !port_unavailable(&error) { return Err(anyhow::Error::new(error).context(format!( "binding port {configured_port}: stop whatever is using it, \ or set a different `port` in gravityd.toml" @@ -143,7 +149,7 @@ async fn bind_with_grace( tracing::info!(port = configured_port, "configured port cleared in time"); return Ok(bound); } - Err(error) if error.kind() == std::io::ErrorKind::AddrInUse => {} + Err(error) if port_unavailable(&error) => {} Err(error) => return Err(error.into()), } } @@ -156,7 +162,7 @@ async fn bind_with_grace( for _ in 0..PORT_NEGOTIATION_ATTEMPTS { match bind_port(addresses, 0, configured_port).await { Ok(bound) => return Ok(bound), - Err(error) if error.kind() == std::io::ErrorKind::AddrInUse => {} + Err(error) if port_unavailable(&error) => {} Err(error) => return Err(error.into()), } } diff --git a/crates/gravityd/src/server/port/tests.rs b/crates/gravityd/src/server/port/tests.rs index 8662ea9..2909ab8 100644 --- a/crates/gravityd/src/server/port/tests.rs +++ b/crates/gravityd/src/server/port/tests.rs @@ -2,6 +2,17 @@ use super::*; const LOOPBACK: IpAddr = IpAddr::V4(std::net::Ipv4Addr::LOCALHOST); +#[test] +fn recognizes_windows_reserved_ports_without_masking_other_errors() { + assert_eq!( + port_unavailable(&std::io::Error::from_raw_os_error(10013)), + cfg!(windows) + ); + assert!(!port_unavailable(&std::io::Error::from( + std::io::ErrorKind::InvalidInput + ))); +} + #[tokio::test] async fn keeps_the_configured_port_when_it_is_available() { let reserved = tokio::net::TcpListener::bind("127.0.0.1:0") diff --git a/crates/gravityd/src/service.rs b/crates/gravityd/src/service.rs index efc3271..547734d 100644 --- a/crates/gravityd/src/service.rs +++ b/crates/gravityd/src/service.rs @@ -12,6 +12,7 @@ use std::process::Command; use anyhow::Context; pub const LAUNCHD_LABEL: &str = "in.mikolajczuk.gravityd"; +pub const SERVICE_LABEL: &str = LAUNCHD_LABEL; const DEFAULT_CONFIG: &str = include_str!("../../../ops/gravityd.example.toml"); /// Where the managed installation lives, derived from the daemon home diff --git a/crates/gravityd/src/service/windows.rs b/crates/gravityd/src/service/windows.rs new file mode 100644 index 0000000..250f584 --- /dev/null +++ b/crates/gravityd/src/service/windows.rs @@ -0,0 +1,240 @@ +//! Per-user Task Scheduler installation; no administrator privileges required. +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::{Duration, Instant}; + +use anyhow::Context; + +pub const SERVICE_LABEL: &str = "Gravity"; +const DEFAULT_CONFIG: &str = include_str!("../../../../ops/gravityd.example.toml"); + +pub struct ServicePaths { + home: PathBuf, +} + +impl ServicePaths { + pub fn new(home: PathBuf, _user_home: PathBuf) -> Self { + Self { home } + } + pub fn bin_path(&self) -> PathBuf { + self.home.join("bin/gravityd.exe") + } + // Kept as the common installation-marker API for the existing CLI. + pub fn plist_path(&self) -> PathBuf { + self.home.join("gravityd-task.xml") + } + pub fn config_path(&self) -> PathBuf { + self.home.join("gravityd.toml") + } + pub fn log_dir(&self) -> PathBuf { + self.home.join("logs") + } + fn launcher_path(&self) -> PathBuf { + self.home.join("gravityd-task.ps1") + } + fn pid_path(&self) -> PathBuf { + self.home.join("gravityd-task.pid") + } +} + +fn task_name(paths: &ServicePaths) -> anyhow::Result { + use sha2::{Digest, Sha256}; + let home = paths.home.canonicalize()?.to_string_lossy().to_lowercase(); + let hash = hex::encode(Sha256::digest(home.as_bytes())); + Ok(format!( + "{SERVICE_LABEL}-{}-{}", + crate::permissions::user_sid()?, + &hash[..12] + )) +} + +fn quote(path: &Path) -> String { + format!( + "'{}'", + path.to_string_lossy() + .replace('/', "\\") + .replace('\'', "''") + ) +} + +fn xml(value: &str) -> String { + value + .replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) + .replace('\'', "'") +} + +fn render_task(paths: &ServicePaths, sid: &str) -> String { + let arguments = format!( + "-NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File \"{}\"", + paths.launcher_path().display() + ); + format!( + r#" + + true{sid} + {sid}InteractiveTokenLeastPrivilege + IgnoreNewfalsefalsetruetruetruePT0SPT1M999 + powershell.exe{arguments} + +"#, + sid = xml(sid), + arguments = xml(&arguments) + ) +} + +fn run_task(args: &[&str]) -> anyhow::Result<()> { + let out = Command::new("schtasks.exe") + .args(args) + .output() + .context("running Task Scheduler")?; + anyhow::ensure!( + out.status.success(), + "Task Scheduler failed: {}", + String::from_utf8_lossy(&out.stderr).trim() + ); + Ok(()) +} + +fn stop(paths: &ServicePaths) -> anyhow::Result<()> { + let name = task_name(paths)?; + // Ending an idle task returns an error; the home lock below verifies stop. + let _ = Command::new("schtasks.exe") + .args(["/end", "/tn", &name]) + .output()?; + if let Ok(pid) = std::fs::read_to_string(paths.pid_path()) { + let pid: u32 = pid.trim().parse().context("invalid managed daemon PID")?; + // Never kill a reused PID belonging to another executable. + let script = format!( + "$p = Get-Process -Id {pid} -ErrorAction SilentlyContinue; if ($p -and $p.Path -eq {}) {{ & taskkill.exe /PID {pid} /T /F | Out-Null; exit $LASTEXITCODE }}", + quote(&paths.bin_path()) + ); + let out = Command::new("powershell.exe") + .args(["-NoProfile", "-NonInteractive", "-Command", &script]) + .output()?; + anyhow::ensure!(out.status.success(), "could not stop managed daemon"); + std::fs::remove_file(paths.pid_path())?; + } + // /end returns before Task Scheduler has finished ending the launcher. + // /run during that interval reports success but IgnoreNew drops the run. + let script = format!( + "$s = New-Object -ComObject Schedule.Service; $s.Connect(); $t = $s.GetFolder('\\').GetTask('{name}'); $until = [DateTime]::UtcNow.AddSeconds(30); while ($t.State -ne 3) {{ if ([DateTime]::UtcNow -ge $until) {{ exit 1 }}; Start-Sleep -Milliseconds 100 }}" + ); + let out = Command::new("powershell.exe") + .args(["-NoProfile", "-NonInteractive", "-Command", &script]) + .output()?; + anyhow::ensure!(out.status.success(), "managed task did not finish stopping"); + let deadline = Instant::now() + Duration::from_secs(10); + loop { + match crate::home::lock(&paths.home) { + Ok(_lock) => return Ok(()), + Err(error) if Instant::now() >= deadline => { + return Err(error).context("waiting for the managed daemon to stop") + } + Err(_) => std::thread::sleep(Duration::from_millis(100)), + } + } +} + +pub fn install(source: &Path, paths: &ServicePaths) -> anyhow::Result<()> { + std::fs::create_dir_all(paths.home.join("bin"))?; + std::fs::create_dir_all(paths.log_dir())?; + if paths.plist_path().exists() { + stop(paths)?; + } + if !paths.config_path().exists() { + std::fs::write(paths.config_path(), DEFAULT_CONFIG)?; + } + if source != paths.bin_path() { + std::fs::copy(source, paths.bin_path()).context("installing daemon binary")?; + } + let launcher = format!( + "$ErrorActionPreference = 'Stop'\n$env:GRAVITY_HOME = {}\n$p = Start-Process -FilePath {} -ArgumentList '--negotiate-port' -WindowStyle Hidden -PassThru -RedirectStandardOutput {} -RedirectStandardError {}\n[IO.File]::WriteAllText({}, [string]$p.Id)\n$p.WaitForExit()\nexit $p.ExitCode\n", + quote(&paths.home), quote(&paths.bin_path()), quote(&paths.log_dir().join("gravityd.out.log")), quote(&paths.log_dir().join("gravityd.err.log")), quote(&paths.pid_path()) + ); + std::fs::write(paths.launcher_path(), launcher)?; + let sid = crate::permissions::user_sid()?; + // schtasks reads task definitions as UTF-16, matching its own exports. + let definition: Vec = std::iter::once(0xfeff) + .chain(render_task(paths, &sid).encode_utf16()) + .flat_map(u16::to_le_bytes) + .collect(); + std::fs::write(paths.plist_path(), definition)?; + let marker = paths.plist_path(); + run_task(&[ + "/create", + "/tn", + &task_name(paths)?, + "/xml", + &marker.to_string_lossy(), + "/f", + ]) +} + +pub fn reload(paths: &ServicePaths) -> anyhow::Result<()> { + anyhow::ensure!( + paths.plist_path().is_file(), + "no managed daemon is installed" + ); + stop(paths)?; + run_task(&["/run", "/tn", &task_name(paths)?]) +} + +pub fn uninstall(paths: &ServicePaths) -> anyhow::Result<()> { + stop(paths)?; + run_task(&["/delete", "/tn", &task_name(paths)?, "/f"])?; + for path in [ + paths.plist_path(), + paths.launcher_path(), + paths.bin_path(), + paths.pid_path(), + crate::home::runtime_port_path(&paths.home), + ] { + if path.exists() { + std::fs::remove_file(path)?; + } + } + Ok(()) +} + +pub fn status(paths: &ServicePaths, configured_port: u16) -> bool { + let port = crate::home::runtime_port(&paths.home).unwrap_or(configured_port); + let installed = paths.bin_path().is_file() && paths.plist_path().is_file(); + let version = crate::server::probe_health(port, Duration::from_secs(2)); + println!( + "task: {}; daemon: {} (127.0.0.1:{port})", + if installed { "installed" } else { "missing" }, + version.as_deref().unwrap_or("unreachable") + ); + installed && version.is_some() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn task_is_scoped_to_current_user_and_escapes_paths() { + let paths = ServicePaths::new( + PathBuf::from(r"C:\Users\Test & User\.gravity"), + PathBuf::new(), + ); + let task = render_task(&paths, "S-1-5-21-123"); + assert!(task.contains("InteractiveToken")); + assert!(task.contains("LeastPrivilege")); + assert!(task.contains("Test & User")); + assert!(task.contains("-WindowStyle Hidden")); + assert!(task.contains("PT0S")); + assert!(task.contains("S-1-5-21-123")); + } + + #[test] + fn launcher_quotes_apostrophes() { + assert_eq!( + quote(Path::new("C:/O'Brien/gravity.exe")), + r"'C:\O''Brien\gravity.exe'" + ); + } +} diff --git a/crates/gravityd/tests/activity.rs b/crates/gravityd/tests/activity.rs index 4460b8f..279bad5 100644 --- a/crates/gravityd/tests/activity.rs +++ b/crates/gravityd/tests/activity.rs @@ -19,7 +19,13 @@ use serde_json::{json, Value}; fn transcript_dir(user_home: &Path, workspace: &str) -> PathBuf { let mangled: String = workspace .chars() - .map(|c| if c == '/' || c == '.' { '-' } else { c }) + .map(|c| { + if c == '/' || c == '.' || cfg!(windows) && (c == '\\' || c == ':') { + '-' + } else { + c + } + }) .collect(); user_home.join(".claude").join("projects").join(mangled) } diff --git a/crates/gravityd/tests/model.rs b/crates/gravityd/tests/model.rs index 499bae5..2bc47d1 100644 --- a/crates/gravityd/tests/model.rs +++ b/crates/gravityd/tests/model.rs @@ -18,7 +18,13 @@ use serde_json::{json, Value}; fn transcript_dir(user_home: &Path, workspace: &str) -> PathBuf { let mangled: String = workspace .chars() - .map(|c| if c == '/' || c == '.' { '-' } else { c }) + .map(|c| { + if c == '/' || c == '.' || cfg!(windows) && (c == '\\' || c == ':') { + '-' + } else { + c + } + }) .collect(); user_home.join(".claude").join("projects").join(mangled) } diff --git a/crates/gravityd/tests/windows_inbox.rs b/crates/gravityd/tests/windows_inbox.rs new file mode 100644 index 0000000..d3b1957 --- /dev/null +++ b/crates/gravityd/tests/windows_inbox.rs @@ -0,0 +1,40 @@ +#![cfg(windows)] +use std::time::Duration; + +use gravityd::runtime::double::DoubleAdapter; +use gravityd::runtime::{BotSpec, RuntimeAdapter, SessionEvent}; + +#[tokio::test] +async fn windows_inbox_delivers_authenticated_unicode_messages() { + let spec = BotSpec { + bot_id: "windows-inbox".into(), + bot_name: "Inbox".into(), + workspace: std::env::temp_dir(), + claude_bin: "unused".into(), + claude_args: Vec::new(), + env: Vec::new(), + cols: 80, + rows: 24, + }; + let mut started = DoubleAdapter.start(&spec).expect("start"); + started.events.recv().await.expect("greeting"); + let socket = started.msg_socket.clone().expect("inbox"); + assert!(socket.path.to_string_lossy().starts_with(r"\\.\pipe\")); + let text = "Hello — 世界\n".repeat(10_000); + let delivered = text.clone(); + tokio::task::spawn_blocking(move || gravityd::channel::send(&socket, &delivered)) + .await + .expect("join") + .expect("deliver"); + let event = tokio::time::timeout(Duration::from_secs(5), started.events.recv()) + .await + .expect("timeout") + .expect("output"); + assert!( + matches!(event, SessionEvent::Output(bytes) if bytes == format!("{text}\r\n").as_bytes()) + ); + let mut socket = started.msg_socket.expect("inbox"); + socket.token = None; + assert!(gravityd::channel::send(&socket, "unauthenticated").is_err()); + started.session.kill().expect("kill"); +} diff --git a/crates/gravityd/tests/windows_pty.rs b/crates/gravityd/tests/windows_pty.rs new file mode 100644 index 0000000..eb75a17 --- /dev/null +++ b/crates/gravityd/tests/windows_pty.rs @@ -0,0 +1,41 @@ +#![cfg(windows)] +use std::time::Duration; + +use gravityd::runtime::pty::PtyAdapter; +use gravityd::runtime::{BotSpec, RuntimeAdapter, SessionEvent}; + +#[tokio::test] +async fn conpty_streams_a_native_process_and_reports_exit() { + let spec = BotSpec { + bot_id: "windows-pty".into(), + bot_name: "PTY".into(), + workspace: std::env::temp_dir(), + claude_bin: "powershell.exe".into(), + claude_args: vec![ + "-NoProfile".into(), + "-Command".into(), + "Write-Output 'gravity-conpty-ok'".into(), + ], + env: Vec::new(), + cols: 80, + rows: 24, + }; + let mut started = PtyAdapter.start(&spec).expect("native ConPTY"); + let collect = async { + let mut bytes = Vec::new(); + while let Some(event) = started.events.recv().await { + match event { + SessionEvent::Output(data) => bytes.extend(data), + SessionEvent::Exited { code } => { + assert_eq!(code, Some(0)); + return bytes; + } + } + } + panic!("process must report exit"); + }; + let bytes = tokio::time::timeout(Duration::from_secs(15), collect) + .await + .expect("PTY exit"); + assert!(String::from_utf8_lossy(&bytes).contains("gravity-conpty-ok")); +} diff --git a/docs/windows.md b/docs/windows.md new file mode 100644 index 0000000..f88c716 --- /dev/null +++ b/docs/windows.md @@ -0,0 +1,60 @@ +# Windows build + +The Windows contribution (#15) builds a native x64 desktop installer and daemon. +Install the stable MSVC Rust toolchain, Visual Studio C++ build tools with the +Windows SDK, Node.js 22, pnpm 11, and Python 3.11 or newer. The desktop uses +WebView2; the installer downloads its bootstrapper when needed. + +From PowerShell at the repository root: + +```powershell +pnpm --dir apps/desktop install --frozen-lockfile +pnpm --dir apps/marketing install --frozen-lockfile +./scripts/prepare-sidecar.ps1 +pnpm --dir apps/desktop tauri build --bundles nsis +``` + +The installer is written under +`apps/desktop/src-tauri/target/release/bundle/nsis/`. Tauri automatically merges +`tauri.windows.conf.json`, which selects a per-user NSIS installer and native +window decorations. The Windows CI workflow checks the daemon, frontend, native +shell, and installer and uploads the installer as a build artifact. It does not +publish a release or subscribe source builds to an update feed. + +On first launch the setup wizard installs the bundled daemon under +`%USERPROFILE%\.gravity\bin\gravityd.exe` and registers a Task Scheduler task +for the current user. It runs immediately and at user sign-in, without elevation, +and leaves bots running when the desktop closes. The task requires an interactive +user session; it does not run while that user is signed out. Logs, configuration, +and state live under `%USERPROFILE%\.gravity`; `GRAVITY_HOME` overrides this root. +If Windows reserves the preferred port (for example for Hyper-V), the local +managed daemon chooses an available port and the desktop follows it. Direct +launches and remotely reachable daemons require an available configured port. + +```powershell +# After installation: +& "$env:USERPROFILE/.gravity/bin/gravityd.exe" service status +& "$env:USERPROFILE/.gravity/bin/gravityd.exe" service restart +& "$env:USERPROFILE/.gravity/bin/gravityd.exe" service uninstall +``` + +Uninstalling the daemon task keeps its database, configuration and workspaces. +The desktop can also connect to a remote daemon using a device token. + +Local Claude Code bots require an installed and authenticated native Claude Code +CLI. Cross-session messaging requires version 2.1.234 or later on native Windows +(2.1.248 or later for all providers). Gravity connects to the authenticated named +pipe reported by its SessionStart hook. PowerShell hooks serialize the pipe path +as JSON and report lifecycle events to the local daemon. A native Windows daemon +cannot use a Claude inbox inside WSL; install the native CLI for native bots. + +For development without model credentials, set `runtime = "double"` in a private +`gravityd.toml` and run `cargo run -p gravityd -- --config `. The double uses +an authenticated named pipe and exercises the same message-delivery path. + +Run `pnpm run verify` before proposing a change. The visual suite uses the pinned +Linux Docker image; Docker Desktop must be running with Linux containers. Git for +Windows supplies Bash for the visual script. Set up the checkout with LF +line endings, as specified in `.gitattributes`, to match the formatting and notice +checks. The shared dependency notice inventory covers macOS ARM64 and Windows +x64; Windows release signing and publication require separate maintainer setup. diff --git a/package.json b/package.json index b455ac2..2710663 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "private": true, "license": "MIT", "scripts": { - "verify": "bash scripts/verify.sh", + "verify": "node scripts/verify.mjs", "notices:generate": "python3 scripts/notices.py --generate", "notices:check": "python3 scripts/notices.py" } diff --git a/scripts/notices.py b/scripts/notices.py index 2259f69..6c3c46d 100644 --- a/scripts/notices.py +++ b/scripts/notices.py @@ -7,12 +7,14 @@ import json import pathlib import re +import shutil import subprocess import tomllib import urllib.request ROOT = pathlib.Path(__file__).resolve().parents[1] TARGET = "aarch64-apple-darwin" +TARGETS = (TARGET, "x86_64-pc-windows-msvc") EXCLUDED = {"other-platform", "build tool (not shipped)"} INVENTORY = ROOT / "third-party/inventory.json" OVERRIDES = ROOT / "third-party/overrides" @@ -31,7 +33,9 @@ "apps/marketing/src/updater-proxy.ts", "apps/marketing/src/updater-bridge.ts", "apps/marketing/wrangler.updater-bridge.jsonc", ".github/workflows/release.yml", "scripts/prepare-sidecar.sh", - "scripts/notices.py", "third-party/README.md", + "scripts/notices.py", "third-party/README.md", "package.json", + "apps/desktop/src-tauri/tauri.windows.conf.json", "scripts/prepare-sidecar.ps1", + ".github/workflows/windows.yml", ] @@ -44,12 +48,18 @@ def notice_text(data): def run(*args): - return subprocess.check_output(args, cwd=ROOT, text=True) + command = shutil.which(args[0]) or args[0] + return subprocess.check_output([command, *args[1:]], cwd=ROOT, text=True, encoding="utf-8") + + +def tracked_bytes(path): + # Git text checkouts may use CRLF; inventory hashes describe committed LF. + return path.read_bytes().replace(b"\r\n", b"\n") def input_hashes(): files = [ROOT / name for name in INPUTS] + sorted(OVERRIDES.rglob("*")) - return {str(p.relative_to(ROOT)): digest(p.read_bytes()) for p in files if p.is_file()} + return {p.relative_to(ROOT).as_posix(): digest(tracked_bytes(p)) for p in files if p.is_file()} def license_files(root): @@ -93,13 +103,15 @@ def toolchain_package(): def rust_packages(manifest, component): data = json.loads(run("cargo", "metadata", "--locked", "--format-version", "1", "--manifest-path", manifest)) - filtered = json.loads(run("cargo", "metadata", "--locked", "--format-version", "1", - "--filter-platform", TARGET, "--manifest-path", manifest)) - selected = {node["id"] for node in filtered["resolve"]["nodes"]} - runtime = run("cargo", "tree", "--locked", "--target", TARGET, - "--manifest-path", manifest, "--edges", "normal,no-proc-macro", - "--prefix", "none", "--format", "{p}").splitlines() - runtime = {line.removesuffix(" (*)") for line in runtime} + selected, runtime = set(), set() + for target in TARGETS: + filtered = json.loads(run("cargo", "metadata", "--locked", "--format-version", "1", + "--filter-platform", target, "--manifest-path", manifest)) + selected.update(node["id"] for node in filtered["resolve"]["nodes"]) + tree = run("cargo", "tree", "--locked", "--target", target, + "--manifest-path", manifest, "--edges", "normal,no-proc-macro", + "--prefix", "none", "--format", "{p}").splitlines() + runtime.update(line.removesuffix(" (*)") for line in tree) lock = tomllib.loads((ROOT / manifest).with_name("Cargo.lock").read_text()) checksums = {(p["name"], p["version"]): p.get("checksum") for p in lock["package"]} for p in data["packages"]: @@ -170,7 +182,7 @@ def generate(): if included: for file in files if files is not None else license_files(root): data = file.read_bytes() - name = str(file.relative_to(root)) + name = file.relative_to(root).as_posix() text = notice_text(data) if file.suffix == ".html": parser = LicenseHTML() @@ -179,7 +191,7 @@ def generate(): texts.append(f"--- {name} ---\n" + text) p["notices"].append({"path": name, "sha256": digest(data)}) for extra in overrides.get(p["name"] + "@" + p["version"], []): - data = (OVERRIDES / extra["file"]).read_bytes() + data = tracked_bytes(OVERRIDES / extra["file"]) texts.append(f"--- {extra['url']} ---\n" + notice_text(data)) p["notices"].append({"url": extra["url"], "sha256": digest(data), "upstream_sha256": extra["upstream_sha256"]}) @@ -210,7 +222,7 @@ def generate(): for component, name in OUTPUTS.items(): content = (f"GRAVITY {component.upper()} THIRD-PARTY NOTICES\n\n" "Gravity's MIT license is separate from these upstream terms.\n" - f"Native inventory target: {TARGET}.\n" + f"Native inventory targets: {', '.join(TARGETS)}.\n" "Runtime dependency closures are conservative; tree-shaken code may be absent.\n" "Build/test notices are included conservatively for generated code; their\n" "presence does not mean the tools themselves are distributed.\n" @@ -219,10 +231,10 @@ def generate(): + "".join(sections[component])) path = ROOT / name path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(content.rstrip() + "\n") + path.write_text(content.rstrip() + "\n", encoding="utf-8", newline="\n") outputs[name] = digest(path.read_bytes()) - INVENTORY.write_text(json.dumps({"target": TARGET, "inputs": input_hashes(), - "outputs": outputs, "packages": records}, indent=2) + "\n") + INVENTORY.write_text(json.dumps({"targets": TARGETS, "inputs": input_hashes(), + "outputs": outputs, "packages": records}, indent=2) + "\n", newline="\n") print(f"Generated {len(records)} inventory records and {len(outputs)} notice bundles.") @@ -231,7 +243,7 @@ def check(check_toolchain=False): if inventory["inputs"] != input_hashes(): raise ValueError("Notice inputs changed. Review scope and run pnpm notices:generate.") for name, expected in inventory["outputs"].items(): - if digest((ROOT / name).read_bytes()) != expected: + if digest(tracked_bytes(ROOT / name)) != expected: raise ValueError("Notice bundle changed: " + name) if set(inventory["outputs"]) != set(OUTPUTS.values()): raise ValueError("Notice output inventory is incomplete") diff --git a/scripts/prepare-sidecar.ps1 b/scripts/prepare-sidecar.ps1 new file mode 100644 index 0000000..0860c8a --- /dev/null +++ b/scripts/prepare-sidecar.ps1 @@ -0,0 +1,20 @@ +# Build and stage the native Windows daemon for Tauri. +[CmdletBinding()] +param() +$ErrorActionPreference = 'Stop' +$repoRoot = Split-Path -Parent $PSScriptRoot +Push-Location $repoRoot +try { + $hostLine = rustc -vV | Where-Object { $_.StartsWith('host: ') } + if ($LASTEXITCODE -ne 0 -or !$hostLine) { throw 'Cannot determine Rust host triple' } + $triple = $hostLine.Substring(6) + if (!$triple.EndsWith('windows-msvc')) { throw 'Use a native Windows MSVC Rust toolchain' } + cargo build --release --locked -p gravityd + if ($LASTEXITCODE -ne 0) { throw 'Daemon build failed' } + $destination = Join-Path $repoRoot 'apps/desktop/src-tauri/binaries' + New-Item -ItemType Directory -Force -Path $destination | Out-Null + Copy-Item -LiteralPath (Join-Path $repoRoot 'target/release/gravityd.exe') -Destination (Join-Path $destination "gravityd-$triple.exe") + Write-Output "Staged gravityd-$triple.exe" +} finally { + Pop-Location +} diff --git a/scripts/prepare-sidecar.sh b/scripts/prepare-sidecar.sh index c91200d..2cafbc3 100755 --- a/scripts/prepare-sidecar.sh +++ b/scripts/prepare-sidecar.sh @@ -8,10 +8,14 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" TRIPLE="$(rustc -vV | sed -n 's/^host: //p')" +EXT="" +case "$TRIPLE" in + *-windows-*) EXT=".exe" ;; +esac cargo build --release -p gravityd --manifest-path "$ROOT/Cargo.toml" DEST="$ROOT/apps/desktop/src-tauri/binaries" mkdir -p "$DEST" -cp "$ROOT/target/release/gravityd" "$DEST/gravityd-$TRIPLE" -echo "staged $DEST/gravityd-$TRIPLE" +cp "$ROOT/target/release/gravityd$EXT" "$DEST/gravityd-$TRIPLE$EXT" +echo "staged $DEST/gravityd-$TRIPLE$EXT" diff --git a/scripts/verify.mjs b/scripts/verify.mjs new file mode 100644 index 0000000..a4a52bf --- /dev/null +++ b/scripts/verify.mjs @@ -0,0 +1,59 @@ +// Run the same repository gates from Windows, macOS, or Linux. +import { spawnSync } from "node:child_process"; +import { existsSync, readFileSync, readdirSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const windows = process.platform === "win32"; +process.chdir(root); +process.env.PYTHONUTF8 ??= "1"; + +function run(program, args = []) { + const result = program === "pnpm" && windows + ? spawnSync("cmd.exe", ["/d", "/s", "/c", `pnpm ${args.join(" ")}`], { cwd: root, stdio: "inherit" }) + : spawnSync(program, args, { cwd: root, stdio: "inherit" }); + if (result.error) { + throw result.error; + } + if (result.status !== 0) { + process.exit(result.status ?? 1); + } +} + +const skillTests = readdirSync(".claude/skills/lib") + .filter((file) => file.endsWith(".test.mjs")) + .map((file) => join(".claude/skills/lib", file)); +run(process.execPath, ["--test", ...skillTests]); +const python = windows ? "python.exe" : "python3"; +run(python, ["scripts/notices.py"]); +run(python, ["-m", "unittest", "discover", "-s", "scripts", "-p", "test_notices.py"]); +run("cargo", ["fmt", "--all", "--check"]); +run("cargo", ["clippy", "--workspace", "--all-targets", "--", "-D", "warnings"]); +run("cargo", ["test", "--workspace", "--all-targets"]); + +const files = spawnSync("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", "*.rs", "*.css"], { encoding: "utf8" }); +if (files.status !== 0) { + throw new Error("Cannot enumerate source files for the length check"); +} +for (const file of files.stdout.split("\0").filter(Boolean)) { + const text = readFileSync(file, "utf8"); + const lines = text.split("\n").length - (text.endsWith("\n") ? 1 : 0); + if (lines > 400) { + throw new Error(`${file}:${lines}: exceeds the 400-line source limit`); + } +} +run("pnpm", ["--dir", "apps/desktop", "check"]); +run("pnpm", ["--dir", "apps/desktop", "build"]); +run("pnpm", ["--dir", "apps/marketing", "check"]); +if (windows) { + run("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "scripts/prepare-sidecar.ps1"]); +} else { + run("bash", ["scripts/prepare-sidecar.sh"]); +} +const native = ["--manifest-path", "apps/desktop/src-tauri/Cargo.toml"]; +run("cargo", ["fmt", ...native, "--check"]); +run("cargo", ["clippy", ...native, "--all-targets", "--", "-D", "warnings"]); +run("cargo", ["test", ...native]); +const gitBash = join(process.env.ProgramFiles ?? "C:/Program Files", "Git/bin/bash.exe"); +run(windows && existsSync(gitBash) ? gitBash : "bash", ["scripts/vr-ci.sh"]); diff --git a/scripts/verify.sh b/scripts/verify.sh index 66e4194..f205b50 100644 --- a/scripts/verify.sh +++ b/scripts/verify.sh @@ -4,21 +4,4 @@ set -euo pipefail cd "$(dirname "${BASH_SOURCE[0]}")/.." -node --test .claude/skills/lib/*.test.mjs -pnpm notices:check -python3 -m unittest discover -s scripts -p 'test_notices.py' -cargo fmt --all --check -cargo clippy --workspace --all-targets -- -D warnings -cargo test --workspace --all-targets -bash scripts/check-file-length.sh - -pnpm --dir apps/desktop check -pnpm --dir apps/desktop build -pnpm --dir apps/marketing check - -bash scripts/prepare-sidecar.sh -cargo fmt --manifest-path apps/desktop/src-tauri/Cargo.toml --check -cargo clippy --manifest-path apps/desktop/src-tauri/Cargo.toml --all-targets -- -D warnings -cargo test --manifest-path apps/desktop/src-tauri/Cargo.toml - -bash scripts/vr-ci.sh +exec node scripts/verify.mjs diff --git a/scripts/vr-ci.sh b/scripts/vr-ci.sh index bc8f855..33948a3 100755 --- a/scripts/vr-ci.sh +++ b/scripts/vr-ci.sh @@ -46,7 +46,14 @@ if [ -r /proc/self/mountinfo ]; then done or the MIT license -, at your -option. +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -======================================================================== -Rust: slab 0.4.12 -Declared license: MIT -Source: https://static.crates.io/crates/slab/slab-0.4.12.crate -Source archive SHA-256: 0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5 -Scope: runtime +1. Definitions. ---- LICENSE --- -Copyright (c) 2019 Carl Lerche + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +--- LICENSE-MIT --- +Copyright (c) 2016 Tomasz Miąsko Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated @@ -25868,10 +25799,55 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ======================================================================== -Rust: smallvec 1.15.2 +Rust: shlex 2.0.1 Declared license: MIT OR Apache-2.0 -Source: https://static.crates.io/crates/smallvec/smallvec-1.15.2.crate -Source archive SHA-256: 8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90 +Source: https://static.crates.io/crates/shlex/shlex-2.0.1.crate +Source archive SHA-256: f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba +Scope: build/test + +--- LICENSE-APACHE --- +Copyright 2015 Nicholas Allegra (comex). + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +--- LICENSE-MIT --- +The MIT License (MIT) + +Copyright (c) 2015 Nicholas Allegra (comex). + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +======================================================================== +Rust: signal-hook-registry 1.4.8 +Declared license: MIT OR Apache-2.0 +Source: https://static.crates.io/crates/signal-hook-registry/signal-hook-registry-1.4.8.crate +Source archive SHA-256: c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b Scope: runtime --- LICENSE-APACHE --- @@ -26078,7 +26054,7 @@ See the License for the specific language governing permissions and limitations under the License. --- LICENSE-MIT --- -Copyright (c) 2018 The Servo Project Developers +Copyright (c) 2017 tokio-jsonrpc developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated @@ -26105,10 +26081,60 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ======================================================================== -Rust: socket2 0.6.5 +Rust: siphasher 1.0.3 +Declared license: MIT/Apache-2.0 +Source: https://static.crates.io/crates/siphasher/siphasher-1.0.3.crate +Source archive SHA-256: 8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649 +Scope: runtime + +--- COPYING --- +Copyright 2012-2016 The Rust Project Developers. +Copyright 2016-2026 Frank Denis. + +Licensed under the Apache License, Version 2.0 or the MIT license +, at your +option. + +======================================================================== +Rust: slab 0.4.12 +Declared license: MIT +Source: https://static.crates.io/crates/slab/slab-0.4.12.crate +Source archive SHA-256: 0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5 +Scope: runtime + +--- LICENSE --- +Copyright (c) 2019 Carl Lerche + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +======================================================================== +Rust: smallvec 1.15.2 Declared license: MIT OR Apache-2.0 -Source: https://static.crates.io/crates/socket2/socket2-0.6.5.crate -Source archive SHA-256: c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4 +Source: https://static.crates.io/crates/smallvec/smallvec-1.15.2.crate +Source archive SHA-256: 8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90 Scope: runtime --- LICENSE-APACHE --- @@ -26315,7 +26341,7 @@ See the License for the specific language governing permissions and limitations under the License. --- LICENSE-MIT --- -Copyright (c) 2014 Alex Crichton +Copyright (c) 2018 The Servo Project Developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated @@ -26342,11 +26368,11 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ======================================================================== -Rust: stable_deref_trait 1.2.1 +Rust: socket2 0.6.5 Declared license: MIT OR Apache-2.0 -Source: https://static.crates.io/crates/stable_deref_trait/stable_deref_trait-1.2.1.crate -Source archive SHA-256: 6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596 -Scope: build/test +Source: https://static.crates.io/crates/socket2/socket2-0.6.5.crate +Source archive SHA-256: c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4 +Scope: runtime --- LICENSE-APACHE --- Apache License @@ -26552,7 +26578,7 @@ See the License for the specific language governing permissions and limitations under the License. --- LICENSE-MIT --- -Copyright (c) 2017 Robert Grosse +Copyright (c) 2014 Alex Crichton Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated @@ -26579,48 +26605,10 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ======================================================================== -Rust: subtle 2.6.1 -Declared license: BSD-3-Clause -Source: https://static.crates.io/crates/subtle/subtle-2.6.1.crate -Source archive SHA-256: 13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292 -Scope: build/test - ---- LICENSE --- -Copyright (c) 2016-2017 Isis Agora Lovecruft, Henry de Valence. All rights reserved. -Copyright (c) 2016-2024 Isis Agora Lovecruft. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - -1. Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright -notice, this list of conditions and the following disclaimer in the -documentation and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS -IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED -TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A -PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED -TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -======================================================================== -Rust: syn 2.0.119 +Rust: stable_deref_trait 1.2.1 Declared license: MIT OR Apache-2.0 -Source: https://static.crates.io/crates/syn/syn-2.0.119.crate -Source archive SHA-256: 872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297 +Source: https://static.crates.io/crates/stable_deref_trait/stable_deref_trait-1.2.1.crate +Source archive SHA-256: 6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596 Scope: build/test --- LICENSE-APACHE --- @@ -26801,7 +26789,34 @@ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION END OF TERMS AND CONDITIONS +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + --- LICENSE-MIT --- +Copyright (c) 2017 Robert Grosse + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the @@ -26827,10 +26842,48 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ======================================================================== -Rust: syn 3.0.4 +Rust: subtle 2.6.1 +Declared license: BSD-3-Clause +Source: https://static.crates.io/crates/subtle/subtle-2.6.1.crate +Source archive SHA-256: 13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292 +Scope: build/test + +--- LICENSE --- +Copyright (c) 2016-2017 Isis Agora Lovecruft, Henry de Valence. All rights reserved. +Copyright (c) 2016-2024 Isis Agora Lovecruft. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +======================================================================== +Rust: syn 2.0.119 Declared license: MIT OR Apache-2.0 -Source: https://static.crates.io/crates/syn/syn-3.0.4.crate -Source archive SHA-256: e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f +Source: https://static.crates.io/crates/syn/syn-2.0.119.crate +Source archive SHA-256: 872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297 Scope: build/test --- LICENSE-APACHE --- @@ -27037,212 +27090,10 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ======================================================================== -Rust: sync_wrapper 1.0.2 -Declared license: Apache-2.0 -Source: https://static.crates.io/crates/sync_wrapper/sync_wrapper-1.0.2.crate -Source archive SHA-256: 0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263 -Scope: runtime - ---- LICENSE --- - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - -======================================================================== -Rust: synstructure 0.13.2 -Declared license: MIT -Source: https://static.crates.io/crates/synstructure/synstructure-0.13.2.crate -Source archive SHA-256: 728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2 -Scope: build/test - ---- LICENSE --- -Copyright 2016 Nika Layzell - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -======================================================================== -Rust: system-configuration 0.7.0 +Rust: syn 3.0.4 Declared license: MIT OR Apache-2.0 -Source: https://static.crates.io/crates/system-configuration/system-configuration-0.7.0.crate -Source archive SHA-256: a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b +Source: https://static.crates.io/crates/syn/syn-3.0.4.crate +Source archive SHA-256: e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f Scope: build/test --- LICENSE-APACHE --- @@ -27423,34 +27274,7 @@ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION END OF TERMS AND CONDITIONS -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - --- LICENSE-MIT --- -Copyright (c) 2024 Mullvad VPN AB - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the @@ -27476,10 +27300,212 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ======================================================================== -Rust: system-configuration-sys 0.6.0 +Rust: sync_wrapper 1.0.2 +Declared license: Apache-2.0 +Source: https://static.crates.io/crates/sync_wrapper/sync_wrapper-1.0.2.crate +Source archive SHA-256: 0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263 +Scope: runtime + +--- LICENSE --- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + +======================================================================== +Rust: synstructure 0.13.2 +Declared license: MIT +Source: https://static.crates.io/crates/synstructure/synstructure-0.13.2.crate +Source archive SHA-256: 728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2 +Scope: build/test + +--- LICENSE --- +Copyright 2016 Nika Layzell + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +======================================================================== +Rust: system-configuration 0.7.0 Declared license: MIT OR Apache-2.0 -Source: https://static.crates.io/crates/system-configuration-sys/system-configuration-sys-0.6.0.crate -Source archive SHA-256: 8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4 +Source: https://static.crates.io/crates/system-configuration/system-configuration-0.7.0.crate +Source archive SHA-256: a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b Scope: build/test --- LICENSE-APACHE --- @@ -27713,10 +27739,10 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ======================================================================== -Rust: tempfile 3.27.0 +Rust: system-configuration-sys 0.6.0 Declared license: MIT OR Apache-2.0 -Source: https://static.crates.io/crates/tempfile/tempfile-3.27.0.crate -Source archive SHA-256: 32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd +Source: https://static.crates.io/crates/system-configuration-sys/system-configuration-sys-0.6.0.crate +Source archive SHA-256: 8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4 Scope: build/test --- LICENSE-APACHE --- @@ -27923,7 +27949,7 @@ See the License for the specific language governing permissions and limitations under the License. --- LICENSE-MIT --- -Copyright (c) 2015 Steven Allen +Copyright (c) 2024 Mullvad VPN AB Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated @@ -27950,40 +27976,277 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ======================================================================== -Rust: termios 0.2.2 -Declared license: MIT -Source: https://static.crates.io/crates/termios/termios-0.2.2.crate -Source archive SHA-256: d5d9cf598a6d7ce700a4e6a9199da127e6819a61e64b68609683cc9a01b5683a -Scope: runtime - ---- LICENSE --- -Copyright (c) 2015 David Cuddeback - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -======================================================================== -Rust: thiserror 1.0.69 +Rust: tempfile 3.27.0 Declared license: MIT OR Apache-2.0 -Source: https://static.crates.io/crates/thiserror/thiserror-1.0.69.crate -Source archive SHA-256: b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52 -Scope: runtime +Source: https://static.crates.io/crates/tempfile/tempfile-3.27.0.crate +Source archive SHA-256: 32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd +Scope: build/test + +--- LICENSE-APACHE --- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +--- LICENSE-MIT --- +Copyright (c) 2015 Steven Allen + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +======================================================================== +Rust: termios 0.2.2 +Declared license: MIT +Source: https://static.crates.io/crates/termios/termios-0.2.2.crate +Source archive SHA-256: d5d9cf598a6d7ce700a4e6a9199da127e6819a61e64b68609683cc9a01b5683a +Scope: runtime + +--- LICENSE --- +Copyright (c) 2015 David Cuddeback + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +======================================================================== +Rust: thiserror 1.0.69 +Declared license: MIT OR Apache-2.0 +Source: https://static.crates.io/crates/thiserror/thiserror-1.0.69.crate +Source archive SHA-256: b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52 +Scope: runtime --- LICENSE-APACHE --- Apache License @@ -33021,6 +33284,1402 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +======================================================================== +Rust: winapi 0.3.9 +Declared license: MIT/Apache-2.0 +Source: https://static.crates.io/crates/winapi/winapi-0.3.9.crate +Source archive SHA-256: 5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419 +Scope: runtime + +--- LICENSE-APACHE --- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--- LICENSE-MIT --- +Copyright (c) 2015-2018 The winapi-rs Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +======================================================================== +Rust: windows-link 0.2.1 +Declared license: MIT OR Apache-2.0 +Source: https://static.crates.io/crates/windows-link/windows-link-0.2.1.crate +Source archive SHA-256: f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5 +Scope: runtime + +--- license-apache-2.0 --- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--- license-mit --- + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + +======================================================================== +Rust: windows-registry 0.6.1 +Declared license: MIT OR Apache-2.0 +Source: https://static.crates.io/crates/windows-registry/windows-registry-0.6.1.crate +Source archive SHA-256: 02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720 +Scope: build/test + +--- license-apache-2.0 --- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--- license-mit --- + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + +======================================================================== +Rust: windows-result 0.4.1 +Declared license: MIT OR Apache-2.0 +Source: https://static.crates.io/crates/windows-result/windows-result-0.4.1.crate +Source archive SHA-256: 7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5 +Scope: build/test + +--- license-apache-2.0 --- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--- license-mit --- + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + +======================================================================== +Rust: windows-strings 0.5.1 +Declared license: MIT OR Apache-2.0 +Source: https://static.crates.io/crates/windows-strings/windows-strings-0.5.1.crate +Source archive SHA-256: 7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091 +Scope: build/test + +--- license-apache-2.0 --- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--- license-mit --- + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + +======================================================================== +Rust: windows-sys 0.61.2 +Declared license: MIT OR Apache-2.0 +Source: https://static.crates.io/crates/windows-sys/windows-sys-0.61.2.crate +Source archive SHA-256: ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc +Scope: runtime + +--- license-apache-2.0 --- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--- license-mit --- + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + ======================================================================== Rust: winnow 0.7.15 Declared license: MIT @@ -33048,6 +34707,34 @@ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +======================================================================== +Rust: winreg 0.10.1 +Declared license: MIT +Source: https://static.crates.io/crates/winreg/winreg-0.10.1.crate +Source archive SHA-256: 80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d +Scope: runtime + +--- LICENSE --- +Copyright (c) 2015 Igor Shaula + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + ======================================================================== Rust: writeable 0.6.4 Declared license: Unicode-3.0 diff --git a/third-party/DESKTOP_NOTICES.txt b/third-party/DESKTOP_NOTICES.txt index 9bf81f2..c88951c 100644 --- a/third-party/DESKTOP_NOTICES.txt +++ b/third-party/DESKTOP_NOTICES.txt @@ -1,7 +1,7 @@ GRAVITY DESKTOP THIRD-PARTY NOTICES Gravity's MIT license is separate from these upstream terms. -Native inventory target: aarch64-apple-darwin. +Native inventory targets: aarch64-apple-darwin, x86_64-pc-windows-msvc. Runtime dependency closures are conservative; tree-shaken code may be absent. Build/test notices are included conservatively for generated code; their presence does not mean the tools themselves are distributed. @@ -7633,6 +7633,216 @@ Repository: git+https://github.com/websockets/ws.git > IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN > CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +======================================================================== +JavaScript: web-vitals 5.3.0 +Declared license: Apache-2.0 +Source: https://registry.npmjs.org/web-vitals/-/web-vitals-5.3.0.tgz +Scope: runtime dependency closure + +--- LICENSE --- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ======================================================================== JavaScript: web-vitals 6.0.0 Declared license: Apache-2.0 @@ -34418,6 +34628,76 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +======================================================================== +Rust: jiff-tzdb 0.1.8 +Declared license: Unlicense OR MIT +Source: https://static.crates.io/crates/jiff-tzdb/jiff-tzdb-0.1.8.crate +Source archive SHA-256: 142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e +Scope: build/test + +--- COPYING --- +This project is dual-licensed under the Unlicense and MIT licenses. + +You may use this code under the terms of either license. + +--- LICENSE-MIT --- +The MIT License (MIT) + +Copyright (c) 2015 Andrew Gallant + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +======================================================================== +Rust: jiff-tzdb-platform 0.1.3 +Declared license: Unlicense OR MIT +Source: https://static.crates.io/crates/jiff-tzdb-platform/jiff-tzdb-platform-0.1.3.crate +Source archive SHA-256: 875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8 +Scope: build/test + +--- COPYING --- +This project is dual-licensed under the Unlicense and MIT licenses. + +You may use this code under the terms of either license. + +--- LICENSE-MIT --- +The MIT License (MIT) + +Copyright (c) 2015 Andrew Gallant + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + ======================================================================== Rust: json-patch 3.0.1 Declared license: MIT/Apache-2.0 @@ -35678,7 +35958,7 @@ Rust: lock_api 0.4.14 Declared license: MIT OR Apache-2.0 Source: https://static.crates.io/crates/lock_api/lock_api-0.4.14.crate Source archive SHA-256: 224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965 -Scope: build/test; bundled daemon: runtime +Scope: runtime; bundled daemon: runtime --- LICENSE-APACHE --- Apache License @@ -40432,7 +40712,7 @@ Rust: parking_lot 0.12.5 Declared license: MIT OR Apache-2.0 Source: https://static.crates.io/crates/parking_lot/parking_lot-0.12.5.crate Source archive SHA-256: 93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a -Scope: build/test; bundled daemon: runtime +Scope: runtime; bundled daemon: runtime --- LICENSE-APACHE --- Apache License @@ -40669,7 +40949,7 @@ Rust: parking_lot_core 0.9.12 Declared license: MIT OR Apache-2.0 Source: https://static.crates.io/crates/parking_lot_core/parking_lot_core-0.9.12.crate Source archive SHA-256: 2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1 -Scope: build/test; bundled daemon: runtime +Scope: runtime; bundled daemon: runtime --- LICENSE-APACHE --- Apache License @@ -49568,6 +49848,22 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +======================================================================== +Rust: schannel 0.1.29 +Declared license: MIT +Source: https://static.crates.io/crates/schannel/schannel-0.1.29.crate +Source archive SHA-256: 91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939 +Scope: bundled daemon: build/test + +--- LICENSE.md --- +Copyright (c) 2015 steffengy + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + ======================================================================== Rust: schemars 0.8.22 Declared license: MIT @@ -49693,7 +49989,7 @@ Rust: scopeguard 1.2.0 Declared license: MIT OR Apache-2.0 Source: https://static.crates.io/crates/scopeguard/scopeguard-1.2.0.crate Source archive SHA-256: 94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49 -Scope: build/test; bundled daemon: runtime +Scope: runtime; bundled daemon: runtime --- LICENSE-APACHE --- Apache License @@ -53923,6 +54219,16 @@ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +======================================================================== +Rust: serial-windows 0.4.0 +Declared license: MIT +Source: https://static.crates.io/crates/serial-windows/serial-windows-0.4.0.crate +Source archive SHA-256: 15c6d3b776267a75d31bbdfd5d36c0ca051251caafc285827052bc53bcdc8162 +Scope: bundled daemon: runtime + +--- LICENSE --- +../LICENSE + ======================================================================== Rust: serialize-to-javascript 0.1.2 Declared license: MIT OR Apache-2.0 @@ -55130,6 +55436,243 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +======================================================================== +Rust: shared_library 0.1.9 +Declared license: Apache-2.0/MIT +Source: https://static.crates.io/crates/shared_library/shared_library-0.1.9.crate +Source archive SHA-256: 5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11 +Scope: bundled daemon: runtime + +--- LICENSE-APACHE --- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +--- LICENSE-MIT --- +Copyright (c) 2017 Pierre Krieger + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + ======================================================================== Rust: shell-words 1.1.1 Declared license: MIT/Apache-2.0 @@ -56203,6 +56746,237 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +======================================================================== +Rust: softbuffer 0.4.8 +Declared license: MIT OR Apache-2.0 +Source: https://static.crates.io/crates/softbuffer/softbuffer-0.4.8.crate +Source archive SHA-256: aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3 +Scope: runtime + +--- LICENSE-APACHE --- +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2022 Kirill Chibisov + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--- LICENSE-MIT --- +Copyright 2022 Kirill Chibisov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + ======================================================================== Rust: stable_deref_trait 1.2.1 Declared license: MIT OR Apache-2.0 @@ -61202,6 +61976,236 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +======================================================================== +Rust: tauri-winrt-notification 0.7.3 +Declared license: MIT OR Apache-2.0 +Source: https://static.crates.io/crates/tauri-winrt-notification/tauri-winrt-notification-0.7.3.crate +Source archive SHA-256: 9ed071c670382e85fc2f48ae706492d8c338f4f89bf72520d32f8abfe880aade +Scope: runtime + +--- LICENSE.spdx --- +SPDXVersion: SPDX-2.1 +DataLicense: CC0-1.0 +PackageName: winrt-notification +DataFormat: SPDXRef-1 +PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy +PackageHomePage: https://tauri.app +PackageLicenseDeclared: Apache-2.0 +PackageLicenseDeclared: MIT +PackageCopyrightText: 2022-2022, The Tauri Programme in the Commons Conservancy +PackageSummary: An incomplete wrapper over the WinRT toast api. + +PackageComment: The package includes the following libraries; see +Relationship information. + +Created: 2019-05-20T09:00:00Z +PackageDownloadLocation: git://github.com/tauri-apps/winrt-notification +PackageDownloadLocation: git+https://github.com/tauri-apps/winrt-notification.git +PackageDownloadLocation: git+ssh://github.com/tauri-apps/winrt-notification.git +Creator: Person: Daniel Thompson-Yvetot + +--- LICENSE_APACHE-2.0 --- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + +--- LICENSE_MIT --- +MIT License + +Copyright (c) 2017 - Present Tauri Apps Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + ======================================================================== Rust: tempfile 3.27.0 Declared license: MIT OR Apache-2.0 @@ -72361,6 +73365,66 @@ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +======================================================================== +Rust: vswhom 0.1.0 +Declared license: MIT +Source: https://static.crates.io/crates/vswhom/vswhom-0.1.0.crate +Source archive SHA-256: be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b +Scope: build/test + +--- LICENSE --- +The MIT License (MIT) + +Copyright (c) 2019 nabijaczleweli + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +======================================================================== +Rust: vswhom-sys 0.1.3 +Declared license: MIT +Source: https://static.crates.io/crates/vswhom-sys/vswhom-sys-0.1.3.crate +Source archive SHA-256: fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150 +Scope: build/test + +--- LICENSE --- +The MIT License (MIT) + +Copyright (c) 2019 nabijaczleweli + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + ======================================================================== Rust: walkdir 2.5.0 Declared license: Unlicense/MIT @@ -72784,276 +73848,5991 @@ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE DATA OR RESULTS, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. -## 5. Definitions +## 5. Definitions + +5.1. "Data" means the material received by a Data Recipient under +this agreement. + +5.2. "Data Provider" means any person who is the source of Data +provided under this agreement and in reliance on a Data Recipient's +agreement to its terms. + +5.3. "Data Recipient" means any person who receives Data directly +or indirectly from a Data Provider and agrees to the terms of this +agreement. + +5.4. "Results" means any outcome obtained by computational analysis +of Data, including for example machine learning models and models' +insights. + +======================================================================== +Rust: webview2-com 0.38.2 +Declared license: MIT +Source: https://static.crates.io/crates/webview2-com/webview2-com-0.38.2.crate +Source archive SHA-256: 7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a +Scope: runtime + +--- https://raw.githubusercontent.com/wravery/webview2-rs/b74dc5e2b394044bea5191052868ce7a106c202c/LICENSE --- +MIT License + +Copyright (c) 2021 Bill Avery + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +======================================================================== +Rust: webview2-com-macros 0.8.1 +Declared license: MIT +Source: https://static.crates.io/crates/webview2-com-macros/webview2-com-macros-0.8.1.crate +Source archive SHA-256: 67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54 +Scope: build/test + +--- https://raw.githubusercontent.com/wravery/webview2-rs/dffa41a8a46d3f5565eefbff2de57d38d399f158/LICENSE --- +MIT License + +Copyright (c) 2021 Bill Avery + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +======================================================================== +Rust: webview2-com-sys 0.38.2 +Declared license: MIT +Source: https://static.crates.io/crates/webview2-com-sys/webview2-com-sys-0.38.2.crate +Source archive SHA-256: 381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c +Scope: runtime + +--- https://raw.githubusercontent.com/wravery/webview2-rs/b74dc5e2b394044bea5191052868ce7a106c202c/LICENSE --- +MIT License + +Copyright (c) 2021 Bill Avery + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +======================================================================== +Rust: winapi 0.3.9 +Declared license: MIT/Apache-2.0 +Source: https://static.crates.io/crates/winapi/winapi-0.3.9.crate +Source archive SHA-256: 5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419 +Scope: bundled daemon: runtime + +--- LICENSE-APACHE --- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--- LICENSE-MIT --- +Copyright (c) 2015-2018 The winapi-rs Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +======================================================================== +Rust: winapi-util 0.1.11 +Declared license: Unlicense OR MIT +Source: https://static.crates.io/crates/winapi-util/winapi-util-0.1.11.crate +Source archive SHA-256: c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22 +Scope: runtime + +--- COPYING --- +This project is dual-licensed under the Unlicense and MIT licenses. + +You may use this code under the terms of either license. + +--- LICENSE-MIT --- +The MIT License (MIT) + +Copyright (c) 2017 Andrew Gallant + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +======================================================================== +Rust: window-vibrancy 0.6.0 +Declared license: Apache-2.0 OR MIT +Source: https://static.crates.io/crates/window-vibrancy/window-vibrancy-0.6.0.crate +Source archive SHA-256: d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c +Scope: runtime + +--- LICENSE-APACHE --- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +--- LICENSE-MIT --- +MIT License + +Copyright (c) 2020-2022 Tauri Programme within The Commons Conservancy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--- LICENSE.spdx --- +SPDXVersion: SPDX-2.1 +DataLicense: CC0-1.0 +PackageName: window-vibrancy +DataFormat: SPDXRef-1 +PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy +PackageHomePage: https://tauri.app +PackageLicenseDeclared: Apache-2.0 +PackageLicenseDeclared: MIT +PackageCopyrightText: 2020-2022, The Tauri Programme in the Commons Conservancy +PackageSummary: Make your windows vibrant. + +PackageComment: The package includes the following libraries; see +Relationship information. + +Created: 2020-05-20T09:00:00Z +PackageDownloadLocation: git://github.com/tauri-apps/window-vibrancy +PackageDownloadLocation: git+https://github.com/tauri-apps/window-vibrancy.git +PackageDownloadLocation: git+ssh://github.com/tauri-apps/window-vibrancy.git +Creator: Person: Daniel Thompson-Yvetot + +======================================================================== +Rust: windows 0.61.3 +Declared license: MIT OR Apache-2.0 +Source: https://static.crates.io/crates/windows/windows-0.61.3.crate +Source archive SHA-256: 9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893 +Scope: runtime + +--- license-apache-2.0 --- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--- license-mit --- + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + +======================================================================== +Rust: windows-collections 0.2.0 +Declared license: MIT OR Apache-2.0 +Source: https://static.crates.io/crates/windows-collections/windows-collections-0.2.0.crate +Source archive SHA-256: 3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8 +Scope: runtime + +--- license-apache-2.0 --- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--- license-mit --- + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + +======================================================================== +Rust: windows-core 0.61.2 +Declared license: MIT OR Apache-2.0 +Source: https://static.crates.io/crates/windows-core/windows-core-0.61.2.crate +Source archive SHA-256: c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3 +Scope: runtime + +--- license-apache-2.0 --- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--- license-mit --- + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + +======================================================================== +Rust: windows-future 0.2.1 +Declared license: MIT OR Apache-2.0 +Source: https://static.crates.io/crates/windows-future/windows-future-0.2.1.crate +Source archive SHA-256: fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e +Scope: runtime + +--- license-apache-2.0 --- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--- license-mit --- + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + +======================================================================== +Rust: windows-implement 0.60.2 +Declared license: MIT OR Apache-2.0 +Source: https://static.crates.io/crates/windows-implement/windows-implement-0.60.2.crate +Source archive SHA-256: 053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf +Scope: build/test + +--- license-apache-2.0 --- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--- license-mit --- + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + +======================================================================== +Rust: windows-interface 0.59.3 +Declared license: MIT OR Apache-2.0 +Source: https://static.crates.io/crates/windows-interface/windows-interface-0.59.3.crate +Source archive SHA-256: 3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358 +Scope: build/test + +--- license-apache-2.0 --- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--- license-mit --- + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + +======================================================================== +Rust: windows-link 0.1.3 +Declared license: MIT OR Apache-2.0 +Source: https://static.crates.io/crates/windows-link/windows-link-0.1.3.crate +Source archive SHA-256: 5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a +Scope: runtime + +--- license-apache-2.0 --- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--- license-mit --- + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + +======================================================================== +Rust: windows-link 0.2.1 +Declared license: MIT OR Apache-2.0 +Source: https://static.crates.io/crates/windows-link/windows-link-0.2.1.crate +Source archive SHA-256: f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5 +Scope: runtime; bundled daemon: runtime + +--- license-apache-2.0 --- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--- license-mit --- + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + +======================================================================== +Rust: windows-numerics 0.2.0 +Declared license: MIT OR Apache-2.0 +Source: https://static.crates.io/crates/windows-numerics/windows-numerics-0.2.0.crate +Source archive SHA-256: 9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1 +Scope: runtime + +--- license-apache-2.0 --- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--- license-mit --- + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + +======================================================================== +Rust: windows-registry 0.6.1 +Declared license: MIT OR Apache-2.0 +Source: https://static.crates.io/crates/windows-registry/windows-registry-0.6.1.crate +Source archive SHA-256: 02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720 +Scope: bundled daemon: build/test + +--- license-apache-2.0 --- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--- license-mit --- + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + +======================================================================== +Rust: windows-result 0.3.4 +Declared license: MIT OR Apache-2.0 +Source: https://static.crates.io/crates/windows-result/windows-result-0.3.4.crate +Source archive SHA-256: 56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6 +Scope: runtime + +--- license-apache-2.0 --- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--- license-mit --- + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + +======================================================================== +Rust: windows-result 0.4.1 +Declared license: MIT OR Apache-2.0 +Source: https://static.crates.io/crates/windows-result/windows-result-0.4.1.crate +Source archive SHA-256: 7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5 +Scope: bundled daemon: build/test + +--- license-apache-2.0 --- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--- license-mit --- + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + +======================================================================== +Rust: windows-strings 0.4.2 +Declared license: MIT OR Apache-2.0 +Source: https://static.crates.io/crates/windows-strings/windows-strings-0.4.2.crate +Source archive SHA-256: 56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57 +Scope: runtime + +--- license-apache-2.0 --- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--- license-mit --- + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + +======================================================================== +Rust: windows-strings 0.5.1 +Declared license: MIT OR Apache-2.0 +Source: https://static.crates.io/crates/windows-strings/windows-strings-0.5.1.crate +Source archive SHA-256: 7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091 +Scope: bundled daemon: build/test + +--- license-apache-2.0 --- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--- license-mit --- + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + +======================================================================== +Rust: windows-sys 0.59.0 +Declared license: MIT OR Apache-2.0 +Source: https://static.crates.io/crates/windows-sys/windows-sys-0.59.0.crate +Source archive SHA-256: 1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b +Scope: runtime + +--- license-apache-2.0 --- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--- license-mit --- + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + +======================================================================== +Rust: windows-sys 0.60.2 +Declared license: MIT OR Apache-2.0 +Source: https://static.crates.io/crates/windows-sys/windows-sys-0.60.2.crate +Source archive SHA-256: f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb +Scope: runtime + +--- license-apache-2.0 --- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--- license-mit --- + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + +======================================================================== +Rust: windows-sys 0.61.2 +Declared license: MIT OR Apache-2.0 +Source: https://static.crates.io/crates/windows-sys/windows-sys-0.61.2.crate +Source archive SHA-256: ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc +Scope: runtime; bundled daemon: runtime + +--- license-apache-2.0 --- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--- license-mit --- + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + +======================================================================== +Rust: windows-targets 0.52.6 +Declared license: MIT OR Apache-2.0 +Source: https://static.crates.io/crates/windows-targets/windows-targets-0.52.6.crate +Source archive SHA-256: 9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973 +Scope: runtime + +--- license-apache-2.0 --- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--- license-mit --- + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + +======================================================================== +Rust: windows-targets 0.53.5 +Declared license: MIT OR Apache-2.0 +Source: https://static.crates.io/crates/windows-targets/windows-targets-0.53.5.crate +Source archive SHA-256: 4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3 +Scope: runtime + +--- license-apache-2.0 --- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--- license-mit --- + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + +======================================================================== +Rust: windows-threading 0.1.0 +Declared license: MIT OR Apache-2.0 +Source: https://static.crates.io/crates/windows-threading/windows-threading-0.1.0.crate +Source archive SHA-256: b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6 +Scope: runtime + +--- license-apache-2.0 --- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--- license-mit --- + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + +======================================================================== +Rust: windows-version 0.1.7 +Declared license: MIT OR Apache-2.0 +Source: https://static.crates.io/crates/windows-version/windows-version-0.1.7.crate +Source archive SHA-256: e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631 +Scope: runtime + +--- license-apache-2.0 --- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--- license-mit --- + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + +======================================================================== +Rust: windows_x86_64_msvc 0.52.6 +Declared license: MIT OR Apache-2.0 +Source: https://static.crates.io/crates/windows_x86_64_msvc/windows_x86_64_msvc-0.52.6.crate +Source archive SHA-256: 589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec +Scope: runtime + +--- license-apache-2.0 --- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. -5.1. "Data" means the material received by a Data Recipient under -this agreement. +--- license-mit --- + MIT License -5.2. "Data Provider" means any person who is the source of Data -provided under this agreement and in reliance on a Data Recipient's -agreement to its terms. + Copyright (c) Microsoft Corporation. -5.3. "Data Recipient" means any person who receives Data directly -or indirectly from a Data Provider and agrees to the terms of this -agreement. + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: -5.4. "Results" means any outcome obtained by computational analysis -of Data, including for example machine learning models and models' -insights. + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE ======================================================================== -Rust: window-vibrancy 0.6.0 -Declared license: Apache-2.0 OR MIT -Source: https://static.crates.io/crates/window-vibrancy/window-vibrancy-0.6.0.crate -Source archive SHA-256: d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c +Rust: windows_x86_64_msvc 0.53.1 +Declared license: MIT OR Apache-2.0 +Source: https://static.crates.io/crates/windows_x86_64_msvc/windows_x86_64_msvc-0.53.1.crate +Source archive SHA-256: d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650 Scope: runtime ---- LICENSE-APACHE --- - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +--- license-apache-2.0 --- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -1. Definitions. + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. + 1. Definitions. - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. -END OF TERMS AND CONDITIONS + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. -APPENDIX: How to apply the Apache License to your work. + END OF TERMS AND CONDITIONS - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. + APPENDIX: How to apply the Apache License to your work. -Copyright [yyyy] [name of copyright owner] + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at + Copyright (c) Microsoft Corporation. - http://www.apache.org/licenses/LICENSE-2.0 + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. + http://www.apache.org/licenses/LICENSE-2.0 ---- LICENSE-MIT --- -MIT License + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. -Copyright (c) 2020-2022 Tauri Programme within The Commons Conservancy +--- license-mit --- + MIT License -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + Copyright (c) Microsoft Corporation. -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. ---- LICENSE.spdx --- -SPDXVersion: SPDX-2.1 -DataLicense: CC0-1.0 -PackageName: window-vibrancy -DataFormat: SPDXRef-1 -PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy -PackageHomePage: https://tauri.app -PackageLicenseDeclared: Apache-2.0 -PackageLicenseDeclared: MIT -PackageCopyrightText: 2020-2022, The Tauri Programme in the Commons Conservancy -PackageSummary: Make your windows vibrant. - -PackageComment: The package includes the following libraries; see -Relationship information. - -Created: 2020-05-20T09:00:00Z -PackageDownloadLocation: git://github.com/tauri-apps/window-vibrancy -PackageDownloadLocation: git+https://github.com/tauri-apps/window-vibrancy.git -PackageDownloadLocation: git+ssh://github.com/tauri-apps/window-vibrancy.git -Creator: Person: Daniel Thompson-Yvetot + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE ======================================================================== Rust: winnow 0.5.40 @@ -73136,6 +79915,62 @@ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +======================================================================== +Rust: winreg 0.10.1 +Declared license: MIT +Source: https://static.crates.io/crates/winreg/winreg-0.10.1.crate +Source archive SHA-256: 80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d +Scope: bundled daemon: runtime + +--- LICENSE --- +Copyright (c) 2015 Igor Shaula + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +======================================================================== +Rust: winreg 0.55.0 +Declared license: MIT +Source: https://static.crates.io/crates/winreg/winreg-0.55.0.crate +Source archive SHA-256: cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97 +Scope: build/test + +--- LICENSE --- +Copyright (c) 2015 Igor Shaula + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + ======================================================================== Rust: writeable 0.6.4 Declared license: Unicode-3.0 @@ -74832,6 +81667,39 @@ SPDX-License-Identifier: Unicode-3.0 Portions of ICU4X may have been adapted from ICU4C and/or ICU4J. ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation and others. +======================================================================== +Rust: zip 4.6.1 +Declared license: MIT +Source: https://static.crates.io/crates/zip/zip-4.6.1.crate +Source archive SHA-256: caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1 +Scope: runtime + +--- LICENSE --- +The MIT License (MIT) + +Copyright (c) 2014 Mathijs van de Nes + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Some files in the "tests/data" subdirectory of this repository are under other +licences; see files named LICENSE.*.txt for details. + ======================================================================== Rust: zlib-rs 0.6.7 Declared license: Zlib diff --git a/third-party/README.md b/third-party/README.md index e807d0f..b3cca2a 100644 --- a/third-party/README.md +++ b/third-party/README.md @@ -1,10 +1,10 @@ # Dependency notices Gravity's `LICENSE` covers Gravity. The separate notice bundles preserve upstream -license, copyright and NOTICE texts for dependencies of the official macOS ARM64 -app and daemon. `inventory.json` records exact versions, provenance, file hashes -and scope against the committed lockfiles. Other distribution targets need their -own review before release. +license, copyright and NOTICE texts for dependencies of the macOS ARM64 and +Windows x64 app and daemon builds. `inventory.json` records exact versions, +provenance, file hashes and scope against the committed lockfiles. Other +distribution targets need their own review before release. ## Distribution scope diff --git a/third-party/inventory.json b/third-party/inventory.json index e821846..77b24db 100644 --- a/third-party/inventory.json +++ b/third-party/inventory.json @@ -1,5 +1,8 @@ { - "target": "aarch64-apple-darwin", + "targets": [ + "aarch64-apple-darwin", + "x86_64-pc-windows-msvc" + ], "inputs": { "Cargo.lock": "ed089d7f431124cb093bc49f5c54a6ca4cb0af573ce8be2f6386475d742fca84", "Cargo.toml": "17f46fbe0ce2a8fab2e1b26a1b10435ac1968ae6f8150db33bb1e05c25fb7491", @@ -19,9 +22,13 @@ "apps/marketing/src/updater-bridge.ts": "9e2cd3920e4fba8444561f2f7393fe6bca641bba528fca3051ac152d28e9e72a", "apps/marketing/wrangler.updater-bridge.jsonc": "31c766018eef47e0b815ccfe2c69123bdb371459de3f99d566bc956780832fb8", ".github/workflows/release.yml": "08d00202ab09a5609acc8db441b73e7a5573f29de5a0884339b0f1759416de5d", - "scripts/prepare-sidecar.sh": "de31795719af5a1f23814b67095ac990cd6a97742b58724ab53512aa2330a313", - "scripts/notices.py": "1fcca820bd7b878e624a74eb24d1cd10d747a2381f08e5918aa12b3402406e65", - "third-party/README.md": "60f83aa879b44f78cb90bd5c8bce90b539490bd529fc411547b54eeccaa0bc97", + "scripts/prepare-sidecar.sh": "8dc536dc0f50c0a8acf9ca6eeea72df78b801ce6a9cdff82c75d7a6c76f8429e", + "scripts/notices.py": "e38d615e2572b5fe96b088c60aabba1f4cc48261df5ecc197691e0f099bc2eb8", + "third-party/README.md": "5f7e36f4b23308dd2208414d119004d27d20095d24a28fd5b84a0fb953c50f9c", + "package.json": "9298a6dafb620f1f77bba3b040a1856bbe3780978db1603eec5b8662bdf8adac", + "apps/desktop/src-tauri/tauri.windows.conf.json": "497e3dff9018b463f4920d78071145dda5eb53439bc031b203ced212a3e9c639", + "scripts/prepare-sidecar.ps1": "ed7a8a164ed95523a9f20a58f8a28107d8976ee364e70e20e9541f3998edf103", + ".github/workflows/windows.yml": "8bbf2d68be3941cbf8b3f2be532f7de030b8bfb16bc53819e49c4335a0ef439a", "third-party/overrides/0322ceec0b8f15aa2d74a6173c07cc8c6f3764b3.txt": "f5c342c49f3ac804f3e8e7bb62a8040a44c50d47bb36902b1abd13f66a1adf8b", "third-party/overrides/11069edd79019f7dafbe3138841cf289209270dd.txt": "8173d5c29b4f956d532781d2b86e4e30f83e6b7878dce18c919451d6ba707c90", "third-party/overrides/16fe87b06e802f094b3fbb0894b137bca2b16ef1.txt": "a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2", @@ -49,9 +56,9 @@ "third-party/overrides/index.json": "688bb4e7bb9089487937265e38e3ad41c60a7f471c33f501a7a2cb426290491c" }, "outputs": { - "third-party/DAEMON_NOTICES.txt": "16edad102976855d4f28dc80c079ffc6c0cc1226b13518ecfa711d88d7dad2f1", - "third-party/DESKTOP_NOTICES.txt": "ecc0405085baa7c72478ff6c842c0ed227212d57502ded763bc702e4c9beb85b", - "apps/marketing/public/THIRD_PARTY_NOTICES.txt": "fc951fe31f6a54f577bfc136177bed747b4c36efc833b0a07413090fa19ad866" + "third-party/DAEMON_NOTICES.txt": "2306e39d572db813bfe0139114f3cd70b4a478dae33b62299191285d750862c9", + "third-party/DESKTOP_NOTICES.txt": "0f1503f1ce5284fdba04876c576c7216f9df2443041dba0c0d3c596763070492", + "apps/marketing/public/THIRD_PARTY_NOTICES.txt": "fe0e343d06cefa62f499f856b77f463d6ef41f7a42f2be4d9726895fc39657a8" }, "packages": [ { @@ -78,13 +85,13 @@ }, { "ecosystem": "JavaScript", - "name": "@cloudflare/workerd-darwin-arm64", + "name": "@cloudflare/workerd-windows-64", "version": "1.20260831.1", "license": "Apache-2.0", "components": { "marketing": "build tool (not shipped)" }, - "source": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260831.1.tgz", + "source": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260831.1.tgz", "notices": [] }, { @@ -111,13 +118,13 @@ }, { "ecosystem": "JavaScript", - "name": "@esbuild/darwin-arm64", + "name": "@esbuild/win32-x64", "version": "0.28.1", "license": "MIT", "components": { "marketing": "build tool (not shipped)" }, - "source": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "source": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", "notices": [] }, { @@ -133,24 +140,13 @@ }, { "ecosystem": "JavaScript", - "name": "@img/sharp-darwin-arm64", + "name": "@img/sharp-win32-x64", "version": "0.35.2", - "license": "Apache-2.0", - "components": { - "marketing": "build tool (not shipped)" - }, - "source": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.2.tgz", - "notices": [] - }, - { - "ecosystem": "JavaScript", - "name": "@img/sharp-libvips-darwin-arm64", - "version": "1.3.1", - "license": "LGPL-3.0-or-later", + "license": "Apache-2.0 AND LGPL-3.0-or-later", "components": { "marketing": "build tool (not shipped)" }, - "source": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.1.tgz", + "source": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.2.tgz", "notices": [] }, { @@ -199,24 +195,24 @@ }, { "ecosystem": "JavaScript", - "name": "@oxfmt/binding-darwin-arm64", + "name": "@oxfmt/binding-win32-x64-msvc", "version": "0.66.0", "license": "MIT", "components": { "marketing": "build tool (not shipped)" }, - "source": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.66.0.tgz", + "source": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.66.0.tgz", "notices": [] }, { "ecosystem": "JavaScript", - "name": "@oxlint/binding-darwin-arm64", + "name": "@oxlint/binding-win32-x64-msvc", "version": "1.81.0", "license": "MIT", "components": { "marketing": "build tool (not shipped)" }, - "source": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.81.0.tgz", + "source": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.81.0.tgz", "notices": [] }, { @@ -320,13 +316,13 @@ }, { "ecosystem": "JavaScript", - "name": "@rolldown/binding-darwin-arm64", + "name": "@rolldown/binding-win32-x64-msvc", "version": "1.2.6", "license": "MIT", "components": { "marketing": "build tool (not shipped)" }, - "source": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.6.tgz", + "source": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.6.tgz", "notices": [] }, { @@ -1177,17 +1173,6 @@ } ] }, - { - "ecosystem": "JavaScript", - "name": "fsevents", - "version": "2.3.3", - "license": "MIT", - "components": { - "marketing": "build tool (not shipped)" - }, - "source": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "notices": [] - }, { "ecosystem": "JavaScript", "name": "hast-util-to-jsx-runtime", @@ -1372,13 +1357,13 @@ }, { "ecosystem": "JavaScript", - "name": "lightningcss-darwin-arm64", + "name": "lightningcss-win32-x64-msvc", "version": "1.33.0", "license": "MPL-2.0", "components": { "marketing": "build tool (not shipped)" }, - "source": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "source": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", "notices": [] }, { @@ -2649,6 +2634,22 @@ "source": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", "notices": [] }, + { + "ecosystem": "JavaScript", + "name": "web-vitals", + "version": "5.3.0", + "license": "Apache-2.0", + "components": { + "desktop": "runtime dependency closure" + }, + "source": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.3.0.tgz", + "notices": [ + { + "path": "LICENSE", + "sha256": "bfdeded4040e05da31ca9b6239dc83bd23fa26ac8db87342a7ca4363f68916ff" + } + ] + }, { "ecosystem": "JavaScript", "name": "web-vitals", @@ -6421,11 +6422,20 @@ "version": "0.1.8", "license": "Unlicense OR MIT", "components": { - "desktop": "other-platform" + "desktop": "build/test" }, "source": "https://static.crates.io/crates/jiff-tzdb/jiff-tzdb-0.1.8.crate", "checksum": "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e", - "notices": [] + "notices": [ + { + "path": "COPYING", + "sha256": "01c266bced4a434da0051174d6bee16a4c82cf634e2679b6155d40d75012390f" + }, + { + "path": "LICENSE-MIT", + "sha256": "0f96a83840e146e43c0ec96a22ec1f392e0680e6c1226e6f3ba87e0740af850f" + } + ] }, { "ecosystem": "Rust", @@ -6433,11 +6443,20 @@ "version": "0.1.3", "license": "Unlicense OR MIT", "components": { - "desktop": "other-platform" + "desktop": "build/test" }, "source": "https://static.crates.io/crates/jiff-tzdb-platform/jiff-tzdb-platform-0.1.3.crate", "checksum": "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8", - "notices": [] + "notices": [ + { + "path": "COPYING", + "sha256": "01c266bced4a434da0051174d6bee16a4c82cf634e2679b6155d40d75012390f" + }, + { + "path": "LICENSE-MIT", + "sha256": "0f96a83840e146e43c0ec96a22ec1f392e0680e6c1226e6f3ba87e0740af850f" + } + ] }, { "ecosystem": "Rust", @@ -6749,7 +6768,7 @@ "license": "MIT OR Apache-2.0", "components": { "daemon": "runtime", - "desktop": "build/test" + "desktop": "runtime" }, "source": "https://static.crates.io/crates/lock_api/lock_api-0.4.14.crate", "checksum": "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965", @@ -7725,7 +7744,7 @@ "license": "MIT OR Apache-2.0", "components": { "daemon": "runtime", - "desktop": "build/test" + "desktop": "runtime" }, "source": "https://static.crates.io/crates/parking_lot/parking_lot-0.12.5.crate", "checksum": "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a", @@ -7747,7 +7766,7 @@ "license": "MIT OR Apache-2.0", "components": { "daemon": "runtime", - "desktop": "build/test" + "desktop": "runtime" }, "source": "https://static.crates.io/crates/parking_lot_core/parking_lot_core-0.9.12.crate", "checksum": "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1", @@ -8977,12 +8996,17 @@ "version": "0.1.29", "license": "MIT", "components": { - "daemon": "other-platform", + "daemon": "build/test", "desktop": "other-platform" }, "source": "https://static.crates.io/crates/schannel/schannel-0.1.29.crate", "checksum": "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939", - "notices": [] + "notices": [ + { + "path": "LICENSE.md", + "sha256": "aa72991ac35b4de0034da0afe943e62b48c4092fc2ba13ae47806d8e9a4ad551" + } + ] }, { "ecosystem": "Rust", @@ -9059,7 +9083,7 @@ "license": "MIT OR Apache-2.0", "components": { "daemon": "runtime", - "desktop": "build/test" + "desktop": "runtime" }, "source": "https://static.crates.io/crates/scopeguard/scopeguard-1.2.0.crate", "checksum": "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49", @@ -9498,11 +9522,16 @@ "version": "0.4.0", "license": "MIT", "components": { - "daemon": "other-platform" + "daemon": "runtime" }, "source": "https://static.crates.io/crates/serial-windows/serial-windows-0.4.0.crate", "checksum": "15c6d3b776267a75d31bbdfd5d36c0ca051251caafc285827052bc53bcdc8162", - "notices": [] + "notices": [ + { + "path": "LICENSE", + "sha256": "1e881ecf2862d01e6e5bc2b861e46886d2a6fb01499c0c508a209a7271b13cf2" + } + ] }, { "ecosystem": "Rust", @@ -9633,11 +9662,20 @@ "version": "0.1.9", "license": "Apache-2.0/MIT", "components": { - "daemon": "other-platform" + "daemon": "runtime" }, "source": "https://static.crates.io/crates/shared_library/shared_library-0.1.9.crate", "checksum": "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11", - "notices": [] + "notices": [ + { + "path": "LICENSE-APACHE", + "sha256": "c144680885b29e4719e2a51f0aab5439a1e02d980692b5aaf086cae12727f28b" + }, + { + "path": "LICENSE-MIT", + "sha256": "1c07d19ccbe2578665ab7d8c63f71559f890eb8d2a82fa39d0206b7a3414064f" + } + ] }, { "ecosystem": "Rust", @@ -9831,11 +9869,20 @@ "version": "0.4.8", "license": "MIT OR Apache-2.0", "components": { - "desktop": "other-platform" + "desktop": "runtime" }, "source": "https://static.crates.io/crates/softbuffer/softbuffer-0.4.8.crate", "checksum": "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3", - "notices": [] + "notices": [ + { + "path": "LICENSE-APACHE", + "sha256": "a4445427440043e8ce6fadf7a43fb906cb0d7f6c573a560a9f142cf9f9bf8ff4" + }, + { + "path": "LICENSE-MIT", + "sha256": "0463bfa47d6d763d8c824333efb0e6d6df3946c13c3b077c78ed9f23ca50d0f2" + } + ] }, { "ecosystem": "Rust", @@ -10466,11 +10513,24 @@ "version": "0.7.3", "license": "MIT OR Apache-2.0", "components": { - "desktop": "other-platform" + "desktop": "runtime" }, "source": "https://static.crates.io/crates/tauri-winrt-notification/tauri-winrt-notification-0.7.3.crate", "checksum": "9ed071c670382e85fc2f48ae706492d8c338f4f89bf72520d32f8abfe880aade", - "notices": [] + "notices": [ + { + "path": "LICENSE.spdx", + "sha256": "a212bb763e1d415210f9d345a4c0bb896571b997f3599a92de16281acafc989b" + }, + { + "path": "LICENSE_APACHE-2.0", + "sha256": "0d542e0c8804e39aa7f37eb00da5a762149dc682d7829451287e11b938e94594" + }, + { + "path": "LICENSE_MIT", + "sha256": "9dd42ea92cff2ede5cd477cbfcce051b2d0115c0ac7f368ee88cb545055dff1d" + } + ] }, { "ecosystem": "Rust", @@ -11901,11 +11961,16 @@ "version": "0.1.0", "license": "MIT", "components": { - "desktop": "other-platform" + "desktop": "build/test" }, "source": "https://static.crates.io/crates/vswhom/vswhom-0.1.0.crate", "checksum": "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b", - "notices": [] + "notices": [ + { + "path": "LICENSE", + "sha256": "b9b7e5d4bf8857061fbecab39ef1923644eba639337af22d7ad0bb9b3ce59c49" + } + ] }, { "ecosystem": "Rust", @@ -11913,11 +11978,16 @@ "version": "0.1.3", "license": "MIT", "components": { - "desktop": "other-platform" + "desktop": "build/test" }, "source": "https://static.crates.io/crates/vswhom-sys/vswhom-sys-0.1.3.crate", "checksum": "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150", - "notices": [] + "notices": [ + { + "path": "LICENSE", + "sha256": "b9b7e5d4bf8857061fbecab39ef1923644eba639337af22d7ad0bb9b3ce59c49" + } + ] }, { "ecosystem": "Rust", @@ -12170,11 +12240,17 @@ "version": "0.38.2", "license": "MIT", "components": { - "desktop": "other-platform" + "desktop": "runtime" }, "source": "https://static.crates.io/crates/webview2-com/webview2-com-0.38.2.crate", "checksum": "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a", - "notices": [] + "notices": [ + { + "url": "https://raw.githubusercontent.com/wravery/webview2-rs/b74dc5e2b394044bea5191052868ce7a106c202c/LICENSE", + "sha256": "0dcf41516e608bbcb6cdc5229feb7b86fe4a643b85e7df251133c93408fdac73", + "upstream_sha256": "0dcf41516e608bbcb6cdc5229feb7b86fe4a643b85e7df251133c93408fdac73" + } + ] }, { "ecosystem": "Rust", @@ -12182,11 +12258,17 @@ "version": "0.8.1", "license": "MIT", "components": { - "desktop": "other-platform" + "desktop": "build/test" }, "source": "https://static.crates.io/crates/webview2-com-macros/webview2-com-macros-0.8.1.crate", "checksum": "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54", - "notices": [] + "notices": [ + { + "url": "https://raw.githubusercontent.com/wravery/webview2-rs/dffa41a8a46d3f5565eefbff2de57d38d399f158/LICENSE", + "sha256": "0dcf41516e608bbcb6cdc5229feb7b86fe4a643b85e7df251133c93408fdac73", + "upstream_sha256": "0dcf41516e608bbcb6cdc5229feb7b86fe4a643b85e7df251133c93408fdac73" + } + ] }, { "ecosystem": "Rust", @@ -12194,11 +12276,17 @@ "version": "0.38.2", "license": "MIT", "components": { - "desktop": "other-platform" + "desktop": "runtime" }, "source": "https://static.crates.io/crates/webview2-com-sys/webview2-com-sys-0.38.2.crate", "checksum": "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c", - "notices": [] + "notices": [ + { + "url": "https://raw.githubusercontent.com/wravery/webview2-rs/b74dc5e2b394044bea5191052868ce7a106c202c/LICENSE", + "sha256": "0dcf41516e608bbcb6cdc5229feb7b86fe4a643b85e7df251133c93408fdac73", + "upstream_sha256": "0dcf41516e608bbcb6cdc5229feb7b86fe4a643b85e7df251133c93408fdac73" + } + ] }, { "ecosystem": "Rust", @@ -12206,12 +12294,21 @@ "version": "0.3.9", "license": "MIT/Apache-2.0", "components": { - "daemon": "other-platform", + "daemon": "runtime", "desktop": "other-platform" }, "source": "https://static.crates.io/crates/winapi/winapi-0.3.9.crate", "checksum": "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419", - "notices": [] + "notices": [ + { + "path": "LICENSE-APACHE", + "sha256": "b40930bbcf80744c86c46a12bc9da056641d722716c378f5659b9e555ef833e1" + }, + { + "path": "LICENSE-MIT", + "sha256": "ce7bc3499fee93d5022ef430d5e4201e79a6d9154f3974e42f41349f0569e09b" + } + ] }, { "ecosystem": "Rust", @@ -12232,11 +12329,20 @@ "version": "0.1.11", "license": "Unlicense OR MIT", "components": { - "desktop": "other-platform" + "desktop": "runtime" }, "source": "https://static.crates.io/crates/winapi-util/winapi-util-0.1.11.crate", "checksum": "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22", - "notices": [] + "notices": [ + { + "path": "COPYING", + "sha256": "01c266bced4a434da0051174d6bee16a4c82cf634e2679b6155d40d75012390f" + }, + { + "path": "LICENSE-MIT", + "sha256": "cb3c929a05e6cbc9de9ab06a4c57eeb60ca8c724bef6c138c87d3a577e27aa14" + } + ] }, { "ecosystem": "Rust", @@ -12282,11 +12388,20 @@ "version": "0.61.3", "license": "MIT OR Apache-2.0", "components": { - "desktop": "other-platform" + "desktop": "runtime" }, "source": "https://static.crates.io/crates/windows/windows-0.61.3.crate", "checksum": "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893", - "notices": [] + "notices": [ + { + "path": "license-apache-2.0", + "sha256": "c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b" + }, + { + "path": "license-mit", + "sha256": "c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383" + } + ] }, { "ecosystem": "Rust", @@ -12294,11 +12409,20 @@ "version": "0.2.0", "license": "MIT OR Apache-2.0", "components": { - "desktop": "other-platform" + "desktop": "runtime" }, "source": "https://static.crates.io/crates/windows-collections/windows-collections-0.2.0.crate", "checksum": "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8", - "notices": [] + "notices": [ + { + "path": "license-apache-2.0", + "sha256": "c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b" + }, + { + "path": "license-mit", + "sha256": "c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383" + } + ] }, { "ecosystem": "Rust", @@ -12306,11 +12430,20 @@ "version": "0.61.2", "license": "MIT OR Apache-2.0", "components": { - "desktop": "other-platform" + "desktop": "runtime" }, "source": "https://static.crates.io/crates/windows-core/windows-core-0.61.2.crate", "checksum": "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3", - "notices": [] + "notices": [ + { + "path": "license-apache-2.0", + "sha256": "c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b" + }, + { + "path": "license-mit", + "sha256": "c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383" + } + ] }, { "ecosystem": "Rust", @@ -12331,11 +12464,20 @@ "version": "0.2.1", "license": "MIT OR Apache-2.0", "components": { - "desktop": "other-platform" + "desktop": "runtime" }, "source": "https://static.crates.io/crates/windows-future/windows-future-0.2.1.crate", "checksum": "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e", - "notices": [] + "notices": [ + { + "path": "license-apache-2.0", + "sha256": "c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b" + }, + { + "path": "license-mit", + "sha256": "c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383" + } + ] }, { "ecosystem": "Rust", @@ -12344,11 +12486,20 @@ "license": "MIT OR Apache-2.0", "components": { "daemon": "other-platform", - "desktop": "other-platform" + "desktop": "build/test" }, "source": "https://static.crates.io/crates/windows-implement/windows-implement-0.60.2.crate", "checksum": "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf", - "notices": [] + "notices": [ + { + "path": "license-apache-2.0", + "sha256": "c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b" + }, + { + "path": "license-mit", + "sha256": "c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383" + } + ] }, { "ecosystem": "Rust", @@ -12357,11 +12508,20 @@ "license": "MIT OR Apache-2.0", "components": { "daemon": "other-platform", - "desktop": "other-platform" + "desktop": "build/test" }, "source": "https://static.crates.io/crates/windows-interface/windows-interface-0.59.3.crate", "checksum": "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358", - "notices": [] + "notices": [ + { + "path": "license-apache-2.0", + "sha256": "c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b" + }, + { + "path": "license-mit", + "sha256": "c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383" + } + ] }, { "ecosystem": "Rust", @@ -12369,11 +12529,20 @@ "version": "0.1.3", "license": "MIT OR Apache-2.0", "components": { - "desktop": "other-platform" + "desktop": "runtime" }, "source": "https://static.crates.io/crates/windows-link/windows-link-0.1.3.crate", "checksum": "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a", - "notices": [] + "notices": [ + { + "path": "license-apache-2.0", + "sha256": "c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b" + }, + { + "path": "license-mit", + "sha256": "c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383" + } + ] }, { "ecosystem": "Rust", @@ -12381,12 +12550,21 @@ "version": "0.2.1", "license": "MIT OR Apache-2.0", "components": { - "daemon": "other-platform", - "desktop": "other-platform" + "daemon": "runtime", + "desktop": "runtime" }, "source": "https://static.crates.io/crates/windows-link/windows-link-0.2.1.crate", "checksum": "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5", - "notices": [] + "notices": [ + { + "path": "license-apache-2.0", + "sha256": "c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b" + }, + { + "path": "license-mit", + "sha256": "c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383" + } + ] }, { "ecosystem": "Rust", @@ -12394,11 +12572,20 @@ "version": "0.2.0", "license": "MIT OR Apache-2.0", "components": { - "desktop": "other-platform" + "desktop": "runtime" }, "source": "https://static.crates.io/crates/windows-numerics/windows-numerics-0.2.0.crate", "checksum": "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1", - "notices": [] + "notices": [ + { + "path": "license-apache-2.0", + "sha256": "c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b" + }, + { + "path": "license-mit", + "sha256": "c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383" + } + ] }, { "ecosystem": "Rust", @@ -12406,11 +12593,20 @@ "version": "0.6.1", "license": "MIT OR Apache-2.0", "components": { - "daemon": "other-platform" + "daemon": "build/test" }, "source": "https://static.crates.io/crates/windows-registry/windows-registry-0.6.1.crate", "checksum": "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720", - "notices": [] + "notices": [ + { + "path": "license-apache-2.0", + "sha256": "c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b" + }, + { + "path": "license-mit", + "sha256": "c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383" + } + ] }, { "ecosystem": "Rust", @@ -12418,11 +12614,20 @@ "version": "0.3.4", "license": "MIT OR Apache-2.0", "components": { - "desktop": "other-platform" + "desktop": "runtime" }, "source": "https://static.crates.io/crates/windows-result/windows-result-0.3.4.crate", "checksum": "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6", - "notices": [] + "notices": [ + { + "path": "license-apache-2.0", + "sha256": "c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b" + }, + { + "path": "license-mit", + "sha256": "c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383" + } + ] }, { "ecosystem": "Rust", @@ -12430,12 +12635,21 @@ "version": "0.4.1", "license": "MIT OR Apache-2.0", "components": { - "daemon": "other-platform", + "daemon": "build/test", "desktop": "other-platform" }, "source": "https://static.crates.io/crates/windows-result/windows-result-0.4.1.crate", "checksum": "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5", - "notices": [] + "notices": [ + { + "path": "license-apache-2.0", + "sha256": "c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b" + }, + { + "path": "license-mit", + "sha256": "c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383" + } + ] }, { "ecosystem": "Rust", @@ -12443,11 +12657,20 @@ "version": "0.4.2", "license": "MIT OR Apache-2.0", "components": { - "desktop": "other-platform" + "desktop": "runtime" }, "source": "https://static.crates.io/crates/windows-strings/windows-strings-0.4.2.crate", "checksum": "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57", - "notices": [] + "notices": [ + { + "path": "license-apache-2.0", + "sha256": "c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b" + }, + { + "path": "license-mit", + "sha256": "c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383" + } + ] }, { "ecosystem": "Rust", @@ -12455,12 +12678,21 @@ "version": "0.5.1", "license": "MIT OR Apache-2.0", "components": { - "daemon": "other-platform", + "daemon": "build/test", "desktop": "other-platform" }, "source": "https://static.crates.io/crates/windows-strings/windows-strings-0.5.1.crate", "checksum": "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091", - "notices": [] + "notices": [ + { + "path": "license-apache-2.0", + "sha256": "c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b" + }, + { + "path": "license-mit", + "sha256": "c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383" + } + ] }, { "ecosystem": "Rust", @@ -12493,11 +12725,20 @@ "version": "0.59.0", "license": "MIT OR Apache-2.0", "components": { - "desktop": "other-platform" + "desktop": "runtime" }, "source": "https://static.crates.io/crates/windows-sys/windows-sys-0.59.0.crate", "checksum": "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b", - "notices": [] + "notices": [ + { + "path": "license-apache-2.0", + "sha256": "c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b" + }, + { + "path": "license-mit", + "sha256": "c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383" + } + ] }, { "ecosystem": "Rust", @@ -12505,11 +12746,20 @@ "version": "0.60.2", "license": "MIT OR Apache-2.0", "components": { - "desktop": "other-platform" + "desktop": "runtime" }, "source": "https://static.crates.io/crates/windows-sys/windows-sys-0.60.2.crate", "checksum": "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb", - "notices": [] + "notices": [ + { + "path": "license-apache-2.0", + "sha256": "c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b" + }, + { + "path": "license-mit", + "sha256": "c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383" + } + ] }, { "ecosystem": "Rust", @@ -12517,12 +12767,21 @@ "version": "0.61.2", "license": "MIT OR Apache-2.0", "components": { - "daemon": "other-platform", - "desktop": "other-platform" + "daemon": "runtime", + "desktop": "runtime" }, "source": "https://static.crates.io/crates/windows-sys/windows-sys-0.61.2.crate", "checksum": "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc", - "notices": [] + "notices": [ + { + "path": "license-apache-2.0", + "sha256": "c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b" + }, + { + "path": "license-mit", + "sha256": "c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383" + } + ] }, { "ecosystem": "Rust", @@ -12543,11 +12802,20 @@ "license": "MIT OR Apache-2.0", "components": { "daemon": "other-platform", - "desktop": "other-platform" + "desktop": "runtime" }, "source": "https://static.crates.io/crates/windows-targets/windows-targets-0.52.6.crate", "checksum": "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973", - "notices": [] + "notices": [ + { + "path": "license-apache-2.0", + "sha256": "c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b" + }, + { + "path": "license-mit", + "sha256": "c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383" + } + ] }, { "ecosystem": "Rust", @@ -12555,11 +12823,20 @@ "version": "0.53.5", "license": "MIT OR Apache-2.0", "components": { - "desktop": "other-platform" + "desktop": "runtime" }, "source": "https://static.crates.io/crates/windows-targets/windows-targets-0.53.5.crate", "checksum": "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3", - "notices": [] + "notices": [ + { + "path": "license-apache-2.0", + "sha256": "c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b" + }, + { + "path": "license-mit", + "sha256": "c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383" + } + ] }, { "ecosystem": "Rust", @@ -12567,11 +12844,20 @@ "version": "0.1.0", "license": "MIT OR Apache-2.0", "components": { - "desktop": "other-platform" + "desktop": "runtime" }, "source": "https://static.crates.io/crates/windows-threading/windows-threading-0.1.0.crate", "checksum": "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6", - "notices": [] + "notices": [ + { + "path": "license-apache-2.0", + "sha256": "c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b" + }, + { + "path": "license-mit", + "sha256": "c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383" + } + ] }, { "ecosystem": "Rust", @@ -12579,11 +12865,20 @@ "version": "0.1.7", "license": "MIT OR Apache-2.0", "components": { - "desktop": "other-platform" + "desktop": "runtime" }, "source": "https://static.crates.io/crates/windows-version/windows-version-0.1.7.crate", "checksum": "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631", - "notices": [] + "notices": [ + { + "path": "license-apache-2.0", + "sha256": "c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b" + }, + { + "path": "license-mit", + "sha256": "c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383" + } + ] }, { "ecosystem": "Rust", @@ -12851,11 +13146,20 @@ "license": "MIT OR Apache-2.0", "components": { "daemon": "other-platform", - "desktop": "other-platform" + "desktop": "runtime" }, "source": "https://static.crates.io/crates/windows_x86_64_msvc/windows_x86_64_msvc-0.52.6.crate", "checksum": "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec", - "notices": [] + "notices": [ + { + "path": "license-apache-2.0", + "sha256": "c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b" + }, + { + "path": "license-mit", + "sha256": "c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383" + } + ] }, { "ecosystem": "Rust", @@ -12863,11 +13167,20 @@ "version": "0.53.1", "license": "MIT OR Apache-2.0", "components": { - "desktop": "other-platform" + "desktop": "runtime" }, "source": "https://static.crates.io/crates/windows_x86_64_msvc/windows_x86_64_msvc-0.53.1.crate", "checksum": "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650", - "notices": [] + "notices": [ + { + "path": "license-apache-2.0", + "sha256": "c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b" + }, + { + "path": "license-mit", + "sha256": "c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383" + } + ] }, { "ecosystem": "Rust", @@ -12927,11 +13240,16 @@ "version": "0.10.1", "license": "MIT", "components": { - "daemon": "other-platform" + "daemon": "runtime" }, "source": "https://static.crates.io/crates/winreg/winreg-0.10.1.crate", "checksum": "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d", - "notices": [] + "notices": [ + { + "path": "LICENSE", + "sha256": "ea021d8c6383c84d19c15ce90d3b1602cf448fdcc0bf3772c98e43c7bf1bf1d1" + } + ] }, { "ecosystem": "Rust", @@ -12939,11 +13257,16 @@ "version": "0.55.0", "license": "MIT", "components": { - "desktop": "other-platform" + "desktop": "build/test" }, "source": "https://static.crates.io/crates/winreg/winreg-0.55.0.crate", "checksum": "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97", - "notices": [] + "notices": [ + { + "path": "LICENSE", + "sha256": "ea021d8c6383c84d19c15ce90d3b1602cf448fdcc0bf3772c98e43c7bf1bf1d1" + } + ] }, { "ecosystem": "Rust", @@ -13357,11 +13680,16 @@ "version": "4.6.1", "license": "MIT", "components": { - "desktop": "other-platform" + "desktop": "runtime" }, "source": "https://static.crates.io/crates/zip/zip-4.6.1.crate", "checksum": "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1", - "notices": [] + "notices": [ + { + "path": "LICENSE", + "sha256": "04dc52136d82400b11df5f474949863a70a5d05d2278dec45b99c479494450a6" + } + ] }, { "ecosystem": "Rust", From e74d9e0fbb732371b98e48eab0561286fc63e4ac Mon Sep 17 00:00:00 2001 From: Manuel Rinaldi Date: Thu, 1 Oct 2026 09:56:00 +0300 Subject: [PATCH 02/55] fix(windows): use platform-neutral setup and folder labels --- apps/desktop/src/components/InfoPanel.test.tsx | 4 ++-- apps/desktop/src/components/InfoPanel.tsx | 4 ++-- apps/desktop/src/components/Sidebar.test.tsx | 2 +- apps/desktop/src/components/setup/SetupScreen.test.tsx | 6 +++--- apps/desktop/src/components/setup/SetupScreen.tsx | 10 +++++----- apps/desktop/src/components/sidebar/useProjectMenu.tsx | 2 +- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/apps/desktop/src/components/InfoPanel.test.tsx b/apps/desktop/src/components/InfoPanel.test.tsx index ee33107..db62798 100644 --- a/apps/desktop/src/components/InfoPanel.test.tsx +++ b/apps/desktop/src/components/InfoPanel.test.tsx @@ -59,11 +59,11 @@ describe("InfoPanel", () => { expect(screen.getByText("ready — idle")).toBeInTheDocument(); }); - it("reveals the bot workspace in Finder", async () => { + it("opens the bot workspace folder", async () => { const user = userEvent.setup(); renderPanel(new FakeDaemon()); - await user.click(screen.getByRole("button", { name: "Reveal bot workspace in Finder" })); + await user.click(screen.getByRole("button", { name: "Open bot workspace folder" })); expect(revealBotWorkspace).toHaveBeenCalledWith("/tmp/alice"); }); diff --git a/apps/desktop/src/components/InfoPanel.tsx b/apps/desktop/src/components/InfoPanel.tsx index 2af1481..4698e1a 100644 --- a/apps/desktop/src/components/InfoPanel.tsx +++ b/apps/desktop/src/components/InfoPanel.tsx @@ -132,8 +132,8 @@ export default function InfoPanel({ + + { + const updated = botSpy(); + render( + , + ); + return updated; +} + +describe("BotRuntimePicker", () => { + it("hides the picker for older daemons", () => { + setup(); + expect(screen.queryByRole("combobox")).not.toBeInTheDocument(); + }); + it("changes runtime only after an explicit save", async () => { + const user = userEvent.setup(); + const daemon = new FakeDaemon().onRequest("set_bot_runtime", () => ({ + type: "bot", + req_id: "1", + bot: bot({ runtime: "codex_cli" }), + })); + daemon.capabilities = ["bot_runtime"]; + const updated = setup(daemon); + expect(screen.getByRole("button", { name: "Change runtime" })).toBeDisabled(); + await user.selectOptions(screen.getByLabelText("Bot runtime"), "codex_cli"); + expect(daemon.requests).toHaveLength(0); + await user.click(screen.getByRole("button", { name: "Change runtime" })); + await waitFor(() => { + expect(updated).toHaveBeenCalledWith(expect.objectContaining({ runtime: "codex_cli" })); + }); + expect(daemon.requests[0]?.body).toEqual({ + type: "set_bot_runtime", + bot_id: "b1", + runtime: "codex_cli", + }); + }); + it.each([ + [false, true], + [true, false], + ])("disables changes without connection or control (%s, %s)", (connected, control) => { + const daemon = new FakeDaemon(); + daemon.capabilities = ["bot_runtime"]; + setup(daemon, connected, control); + expect(screen.getByRole("combobox")).toBeDisabled(); + expect(screen.getByRole("button", { name: "Change runtime" })).toBeDisabled(); + }); + it("reports failed switches and lets the user retry", async () => { + const user = userEvent.setup(); + const toast = toastSpy(); + const daemon = new FakeDaemon().onRequest("set_bot_runtime", () => { + throw new Error("CLI unavailable"); + }); + daemon.capabilities = ["bot_runtime"]; + render( + , + ); + await user.selectOptions(screen.getByRole("combobox"), "codex_cli"); + await user.click(screen.getByRole("button", { name: "Change runtime" })); + await waitFor(() => { + expect(toast).toHaveBeenCalledWith("error", "Runtime update failed", "CLI unavailable"); + }); + expect(screen.getByRole("button", { name: "Change runtime" })).toBeEnabled(); + }); +}); diff --git a/apps/desktop/src/components/bot/BotRuntimePicker.tsx b/apps/desktop/src/components/bot/BotRuntimePicker.tsx new file mode 100644 index 0000000..a8b874c --- /dev/null +++ b/apps/desktop/src/components/bot/BotRuntimePicker.tsx @@ -0,0 +1,86 @@ +import { useState } from "react"; +import type { ReactElement } from "react"; +import type { DaemonApi } from "../../protocol/api"; +import type { Bot, BotRuntime, NotifyLevel } from "../../protocol/entities"; +import { errText } from "../../util"; + +interface Props { + readonly client: DaemonApi; + readonly bot: Bot; + readonly connected: boolean; + readonly canControl: boolean; + readonly onBotUpdated: (bot: Bot) => void; + readonly onToast: (level: NotifyLevel, title: string, body: string) => void; +} + +export default function BotRuntimePicker({ + client, + bot, + connected, + canControl, + onBotUpdated, + onToast, +}: Props): ReactElement | null { + const current = bot.runtime ?? "claude_code"; + const [runtime, setRuntime] = useState(current); + const [saving, setSaving] = useState(false); + if (!client.capabilities.includes("bot_runtime")) { + return null; + } + const save = async (): Promise => { + setSaving(true); + try { + const reply = await client.request( + { type: "set_bot_runtime", bot_id: bot.id, runtime }, + "bot", + ); + onBotUpdated(reply.bot); + onToast( + "info", + "Runtime updated", + `${reply.bot.name} will restart with ${runtime === "codex_cli" ? "Codex CLI" : "Claude Code"}.`, + ); + } catch (error) { + onToast("error", "Runtime update failed", errText(error)); + } finally { + setSaving(false); + } + }; + return ( +
+ + + + Changing runtime interrupts the current turn. Both use the same workspace and saved facts; + each keeps its own conversation history. Install and sign in to the selected CLI first. + +
+ +
+
+ ); +} diff --git a/apps/desktop/src/protocol/entities.ts b/apps/desktop/src/protocol/entities.ts index fce815e..41baf70 100644 --- a/apps/desktop/src/protocol/entities.ts +++ b/apps/desktop/src/protocol/entities.ts @@ -29,7 +29,11 @@ export type BotState = | "stopping" | "stopped"; +export type BotRuntime = "claude_code" | "codex_cli"; + export interface Bot { + /** Absent on older daemons, which use Claude Code. */ + readonly runtime?: BotRuntime; readonly id: string; readonly project_id: string; readonly name: string; diff --git a/apps/desktop/src/protocol/requests.ts b/apps/desktop/src/protocol/requests.ts index 40db2a3..288e7e3 100644 --- a/apps/desktop/src/protocol/requests.ts +++ b/apps/desktop/src/protocol/requests.ts @@ -1,7 +1,13 @@ // Client → server frames (protocol v2). import type { DecisionRequestBody } from "./decisionRequests"; -import type { DeliveryState, DeviceCapability, OverlapPolicy, RoutineTrigger } from "./entities"; +import type { + BotRuntime, + DeliveryState, + DeviceCapability, + OverlapPolicy, + RoutineTrigger, +} from "./entities"; export type ClientRequestBody = | DecisionRequestBody @@ -22,6 +28,7 @@ export type ClientRequestBody = | { readonly type: "list_bots"; readonly project_id?: string } | { readonly type: "create_bot"; + readonly runtime?: BotRuntime; readonly project_id: string; /** Omit for an automatically numbered "New Bot" placeholder. */ readonly name?: string; @@ -37,6 +44,11 @@ export type ClientRequestBody = readonly instructions?: string; readonly avatar?: string; } + | { + readonly type: "set_bot_runtime"; + readonly bot_id: string; + readonly runtime: BotRuntime; + } | { readonly type: "delete_bot"; readonly bot_id: string; diff --git a/crates/bus/src/schema.rs b/crates/bus/src/schema.rs index edc2891..10283ed 100644 --- a/crates/bus/src/schema.rs +++ b/crates/bus/src/schema.rs @@ -28,4 +28,5 @@ pub const MIGRATIONS: &[&str] = &[ MIGRATION_10, MIGRATION_11, MIGRATION_12, + "ALTER TABLE bot ADD COLUMN runtime TEXT NOT NULL DEFAULT 'claude_code' CHECK(runtime IN ('claude_code', 'codex_cli'));", ]; diff --git a/crates/bus/src/types/entities.rs b/crates/bus/src/types/entities.rs index 36e66a9..a86827a 100644 --- a/crates/bus/src/types/entities.rs +++ b/crates/bus/src/types/entities.rs @@ -33,6 +33,8 @@ pub struct Bot { pub description: String, pub avatar: String, pub instructions: String, + #[serde(default)] + pub runtime: BotRuntime, pub state: BotState, pub state_reason: String, pub unread_count: i64, @@ -52,6 +54,23 @@ pub struct Bot { pub created_at: DateTime, } +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum BotRuntime { + #[default] + ClaudeCode, + CodexCli, +} + +impl BotRuntime { + pub fn as_str(self) -> &'static str { + match self { + Self::ClaudeCode => "claude_code", + Self::CodexCli => "codex_cli", + } + } +} + /// One field-level change to a bot's identity: the audit trail that makes /// unsupervised self-management reversible. #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/crates/gravityd/src/activity.rs b/crates/gravityd/src/activity.rs index e8288b4..bb70464 100644 --- a/crates/gravityd/src/activity.rs +++ b/crates/gravityd/src/activity.rs @@ -161,7 +161,11 @@ fn assistant_text(entry: &Value) -> Option { /// has no transcript yet (never started, or never spoke). pub fn from_transcript(home: &Path, workspace: &Path) -> Option { let path = newest_transcript(&transcript_dir(home, workspace))?; - for line in tail_lines(&path, SCAN_LINES) { + from_transcript_file(&path) +} + +fn from_transcript_file(path: &Path) -> Option { + for line in tail_lines(path, SCAN_LINES) { let Ok(entry) = serde_json::from_str::(&line) else { continue; }; @@ -200,7 +204,13 @@ fn from_bus(db: &Db, bot_id: &str) -> Option { /// One preview line for a bot: the newer of its last Claude Code turn and its /// last bus message. None when the bot has said nothing yet. pub fn for_bot(db: &Db, home: &Path, bot_id: &str, workspace: &Path) -> Option { - newer(from_transcript(home, workspace), from_bus(db, bot_id)) + let transcript = match db.get_bot(bot_id).ok().flatten().map(|bot| bot.runtime) { + Some(bus::BotRuntime::CodexCli) => { + from_transcript_file(&crate::runtime::codex::transcript_path(workspace)) + } + _ => from_transcript(home, workspace), + }; + newer(transcript, from_bus(db, bot_id)) } /// Collapses whitespace and caps the length, so one preview stays one line. diff --git a/crates/gravityd/src/app.rs b/crates/gravityd/src/app.rs index 3340061..be3448a 100644 --- a/crates/gravityd/src/app.rs +++ b/crates/gravityd/src/app.rs @@ -8,7 +8,7 @@ use crate::db::Db; use crate::events::Events; use crate::overrides::{AutoCompactOverride, AUTO_COMPACT_META_KEY}; use crate::runtime::double::DoubleAdapter; -use crate::runtime::pty::PtyAdapter; +use crate::runtime::mixed::MixedAdapter; use crate::runtime::RuntimeAdapter; use crate::secrets::Secrets; use crate::supervisor::Supervisor; @@ -27,6 +27,7 @@ pub const CAPABILITIES: &[&str] = &[ "bot_self_management", "config", "decisions", + "bot_runtime", ]; pub struct AppState { @@ -52,7 +53,7 @@ impl AppState { let secrets = Arc::new(Secrets::open(&cfg.secrets_dir())?); let events = Events::new(); let adapter: Arc = match cfg.runtime { - RuntimeKind::Pty => Arc::new(PtyAdapter), + RuntimeKind::Pty => Arc::new(MixedAdapter::new(&cfg)), RuntimeKind::Double => Arc::new(DoubleAdapter), }; let auto_compact = AutoCompactOverride::default(); diff --git a/crates/gravityd/src/botmgmt.rs b/crates/gravityd/src/botmgmt.rs index 01b23ca..9c6f1b5 100644 --- a/crates/gravityd/src/botmgmt.rs +++ b/crates/gravityd/src/botmgmt.rs @@ -22,7 +22,7 @@ mod create; mod revert; pub use archive::{archive_bot, prune_archived_workspaces}; -pub use create::{create_bot, Created}; +pub use create::{create_bot, create_bot_with_runtime, Created}; pub use revert::revert_revision; /// Fields that may change on a bot. `None` leaves the stored value alone, diff --git a/crates/gravityd/src/botmgmt/create.rs b/crates/gravityd/src/botmgmt/create.rs index 25a1c1f..8584a50 100644 --- a/crates/gravityd/src/botmgmt/create.rs +++ b/crates/gravityd/src/botmgmt/create.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use anyhow::bail; -use bus::{Bot, MessageKind, RevisionField}; +use bus::{Bot, BotRuntime, MessageKind, RevisionField}; use rand::seq::SliceRandom; use crate::app::AppState; @@ -50,6 +50,26 @@ pub fn create_bot( edit: &IdentityEdit<'_>, creator: Option<&Bot>, actor: &Actor<'_>, +) -> anyhow::Result { + create_bot_with_runtime( + app, + project_id, + edit, + creator, + actor, + creator + .map(|b| b.runtime) + .unwrap_or(app.cfg.default_bot_runtime), + ) +} + +pub fn create_bot_with_runtime( + app: &Arc, + project_id: &str, + edit: &IdentityEdit<'_>, + creator: Option<&Bot>, + actor: &Actor<'_>, + runtime: BotRuntime, ) -> anyhow::Result { let raw_name = edit.name.unwrap_or_default(); let name = validate_name(app, project_id, raw_name, None)?; @@ -89,7 +109,7 @@ pub fn create_bot( let description = charter::description(edit.description); let instructions = charter::instructions(edit.instructions, creator.map(|c| c.name.as_str())); - let bot = app.db.create_bot( + let bot = app.db.create_bot_with_runtime( project_id, &name, &description, @@ -98,13 +118,14 @@ pub fn create_bot( &workspace.display().to_string(), &dir_name, creator.map(|c| c.id.as_str()), + runtime, )?; // Issuing the token before provisioning means the runtime can authenticate // as soon as the files land. app.secrets.bot_token(&bot.id)?; let dirs = paths::provision_bot(&app.cfg, &provision_spec(app, &project, &bot))?; - if app.cfg.runtime == RuntimeKind::Pty { + if app.cfg.runtime == RuntimeKind::Pty && bot.runtime == BotRuntime::ClaudeCode { if let Err(error) = paths::trust_workspace(&app.cfg.user_home, &dirs.workspace) { tracing::warn!(bot_id = %bot.id, %error, "could not trust new bot workspace"); } diff --git a/crates/gravityd/src/config.rs b/crates/gravityd/src/config.rs index fa4a1e8..ac6b99c 100644 --- a/crates/gravityd/src/config.rs +++ b/crates/gravityd/src/config.rs @@ -37,11 +37,14 @@ pub struct Config { /// where only the allowlisted `/mcp` URL reaches bots at all. Ignored for /// direct launches, which always fail on a collision. pub negotiate_port: bool, - /// `pty` (spawn `claude` in a pty) or `double` (deterministic test runtime). + /// `pty` (each bot's saved CLI) or `double` (deterministic test runtime). pub runtime: RuntimeKind, pub claude_bin: String, /// Extra arguments passed to the `claude` CLI. pub claude_args: Vec, + pub codex_bin: String, + pub codex_args: Vec, + pub default_bot_runtime: bus::BotRuntime, /// Context window, in tokens, a bot session may grow to before Claude Code /// auto-compacts it (`CLAUDE_CODE_AUTO_COMPACT_WINDOW`). Bots are /// always-on chat sessions, and every turn re-sends the whole transcript, @@ -170,6 +173,9 @@ impl Default for Config { runtime: RuntimeKind::Pty, claude_bin: "claude".to_string(), claude_args: Vec::new(), + codex_bin: "codex".to_string(), + codex_args: Vec::new(), + default_bot_runtime: bus::BotRuntime::ClaudeCode, auto_compact_window: Some(DEFAULT_AUTO_COMPACT_WINDOW), classic_renderer: true, max_bots_per_project: 12, diff --git a/crates/gravityd/src/db.rs b/crates/gravityd/src/db.rs index 9eff0c9..4bdafd5 100644 --- a/crates/gravityd/src/db.rs +++ b/crates/gravityd/src/db.rs @@ -15,6 +15,7 @@ pub use routines::RoutineLimits; pub use runs::NewRun; pub use signals::NewSignal; +mod bot_runtime; mod bots; mod conversations; mod decision_threads; diff --git a/crates/gravityd/src/db/bot_runtime.rs b/crates/gravityd/src/db/bot_runtime.rs new file mode 100644 index 0000000..aa1698b --- /dev/null +++ b/crates/gravityd/src/db/bot_runtime.rs @@ -0,0 +1,99 @@ +use bus::{new_id, now, Bot, BotRuntime, BotState}; +use rusqlite::params; + +use super::{ts, Db}; + +impl Db { + #[allow(clippy::too_many_arguments)] + pub fn create_bot( + &self, + project_id: &str, + name: &str, + description: &str, + instructions: &str, + avatar: &str, + workspace_path: &str, + dir_name: &str, + created_by_bot_id: Option<&str>, + ) -> anyhow::Result { + self.create_bot_with_runtime( + project_id, + name, + description, + instructions, + avatar, + workspace_path, + dir_name, + created_by_bot_id, + BotRuntime::ClaudeCode, + ) + } + + #[allow(clippy::too_many_arguments)] + pub fn create_bot_with_runtime( + &self, + project_id: &str, + name: &str, + description: &str, + instructions: &str, + avatar: &str, + workspace_path: &str, + dir_name: &str, + created_by_bot_id: Option<&str>, + runtime: BotRuntime, + ) -> anyhow::Result { + let bot = Bot { + id: new_id(), + project_id: project_id.to_string(), + name: name.to_string(), + description: description.to_string(), + avatar: avatar.to_string(), + instructions: instructions.to_string(), + runtime, + state: BotState::Stopped, + state_reason: String::new(), + unread_count: 0, + workspace_path: workspace_path.to_string(), + dir_name: dir_name.to_string(), + created_by_bot_id: created_by_bot_id.map(|s| s.to_string()), + deleted_at: None, + created_at: now(), + }; + let conn = self.lock(); + conn.execute( + "INSERT INTO bot(id, project_id, name, description, avatar, instructions, + workspace_path, created_at, dir_name, created_by_bot_id, runtime) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", + params![ + bot.id, + bot.project_id, + bot.name, + bot.description, + bot.avatar, + bot.instructions, + bot.workspace_path, + ts(bot.created_at), + bot.dir_name, + bot.created_by_bot_id, + bot.runtime.as_str() + ], + )?; + // A DM conversation exists for every bot from creation. + let conv_id = new_id(); + conn.execute( + "INSERT INTO conversation(id, project_id, kind, bot_id, title, created_at) + VALUES (?1, ?2, 'dm', ?3, ?4, ?5)", + params![conv_id, bot.project_id, bot.id, bot.name, ts(now())], + )?; + Ok(bot) + } + + pub fn set_bot_runtime(&self, bot_id: &str, runtime: BotRuntime) -> anyhow::Result<()> { + let changed = self.lock().execute( + "UPDATE bot SET runtime = ?2 WHERE id = ?1 AND deleted_at IS NULL", + params![bot_id, runtime.as_str()], + )?; + anyhow::ensure!(changed == 1, "bot not found or archived"); + Ok(()) + } +} diff --git a/crates/gravityd/src/db/bots.rs b/crates/gravityd/src/db/bots.rs index 44c06ce..0241812 100644 --- a/crates/gravityd/src/db/bots.rs +++ b/crates/gravityd/src/db/bots.rs @@ -31,6 +31,10 @@ impl Db { dir_name: r.get(8)?, created_by_bot_id: r.get(9)?, deleted_at: r.get::<_, Option>(10)?.map(|s| parse_ts(&s)), + runtime: match r.get::<_, String>(11)?.as_str() { + "codex_cli" => BotRuntime::CodexCli, + _ => BotRuntime::ClaudeCode, + }, // Runtime fields are overlaid by the supervisor. state: BotState::Stopped, state_reason: String::new(), @@ -39,67 +43,11 @@ impl Db { } const BOT_COLS: &'static str = "id, project_id, name, description, avatar, instructions, \ - workspace_path, created_at, dir_name, created_by_bot_id, deleted_at"; + workspace_path, created_at, dir_name, created_by_bot_id, deleted_at, runtime"; /// Restricts a query to bots that still exist for addressing purposes. const LIVE: &'static str = "deleted_at IS NULL"; - #[allow(clippy::too_many_arguments)] - pub fn create_bot( - &self, - project_id: &str, - name: &str, - description: &str, - instructions: &str, - avatar: &str, - workspace_path: &str, - dir_name: &str, - created_by_bot_id: Option<&str>, - ) -> anyhow::Result { - let bot = Bot { - id: new_id(), - project_id: project_id.to_string(), - name: name.to_string(), - description: description.to_string(), - avatar: avatar.to_string(), - instructions: instructions.to_string(), - state: BotState::Stopped, - state_reason: String::new(), - unread_count: 0, - workspace_path: workspace_path.to_string(), - dir_name: dir_name.to_string(), - created_by_bot_id: created_by_bot_id.map(|s| s.to_string()), - deleted_at: None, - created_at: now(), - }; - let conn = self.lock(); - conn.execute( - "INSERT INTO bot(id, project_id, name, description, avatar, instructions, - workspace_path, created_at, dir_name, created_by_bot_id) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)", - params![ - bot.id, - bot.project_id, - bot.name, - bot.description, - bot.avatar, - bot.instructions, - bot.workspace_path, - ts(bot.created_at), - bot.dir_name, - bot.created_by_bot_id - ], - )?; - // A DM conversation exists for every bot from creation. - let conv_id = new_id(); - conn.execute( - "INSERT INTO conversation(id, project_id, kind, bot_id, title, created_at) - VALUES (?1, ?2, 'dm', ?3, ?4, ?5)", - params![conv_id, bot.project_id, bot.id, bot.name, ts(now())], - )?; - Ok(bot) - } - /// Record that the bot's workspace now holds a Claude Code conversation, /// so later starts resume it instead of opening a blank session. pub fn mark_bot_session(&self, bot_id: &str) -> anyhow::Result<()> { diff --git a/crates/gravityd/src/runtime/codex.rs b/crates/gravityd/src/runtime/codex.rs new file mode 100644 index 0000000..2440e8a --- /dev/null +++ b/crates/gravityd/src/runtime/codex.rs @@ -0,0 +1,197 @@ +//! Codex CLI App Server over stdio JSONL. Bus input is a separate RPC stream. +use std::io::{BufRead, BufReader}; +use std::path::{Path, PathBuf}; +use std::process::{Child, Command, Stdio}; +use std::sync::{mpsc, Arc, Mutex}; +use std::time::Duration; + +use super::{BotSpec, Capabilities, RuntimeAdapter, RuntimeSession, SessionEvent, StartedSession}; +use anyhow::Context; +use serde_json::Value; + +mod approvals; +mod observations; +mod worker; + +#[derive(Debug, Clone)] +pub struct CodexSpec { + pub bin: String, + pub args: Vec, + pub port: u16, + pub artifacts: Option, +} + +pub struct CodexAdapter; + +pub(super) enum Wire { + Server(Value), + Closed, + Input(Vec), + Deliver(String, mpsc::SyncSender>), + Stop, +} + +pub fn transcript_path(workspace: &Path) -> PathBuf { + workspace + .parent() + .unwrap_or(workspace) + .join("codex-observations.jsonl") +} + +impl RuntimeAdapter for CodexAdapter { + fn capabilities(&self) -> Capabilities { + Capabilities { + kind: "codex_cli", + native_background: false, + channel_delivery: true, + permission_relay: true, + } + } + fn probe(&self) -> anyhow::Result { + let out = Command::new("codex").arg("--version").output()?; + anyhow::ensure!(out.status.success(), "codex --version failed"); + Ok(String::from_utf8_lossy(&out.stdout).trim().to_string()) + } + fn start(&self, spec: &BotSpec) -> anyhow::Result { + let codex = spec + .codex + .as_ref() + .context("missing Codex runtime settings")?; + let mut command = Command::new(&codex.bin); + command + .args(&codex.args) + .arg("app-server") + .args(["--listen", "stdio://"]) + .current_dir(&spec.workspace) + .envs(spec.env.iter().cloned()) + .env_remove("CODEX_THREAD_ID") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + command.creation_flags(0x08000000); + } + let mut child = command.spawn().context("spawn Codex CLI App Server")?; + let stdin = child.stdin.take().context("Codex stdin")?; + let stdout = child.stdout.take().context("Codex stdout")?; + let stderr = child.stderr.take().context("Codex stderr")?; + let child = Arc::new(Mutex::new(child)); + let (tx, rx) = mpsc::channel(); + let (events_tx, events_rx) = tokio::sync::mpsc::unbounded_channel(); + let reader_tx = tx.clone(); + std::thread::spawn(move || { + let mut reader = BufReader::new(stdout); + loop { + let mut line = String::new(); + match reader.read_line(&mut line) { + Ok(0) | Err(_) => break, + Ok(_) => match serde_json::from_str(&line) { + Ok(value) => { + if reader_tx.send(Wire::Server(value)).is_err() { + break; + } + } + Err(_) => break, + }, + } + } + let _ = reader_tx.send(Wire::Closed); + }); + let err_tx = events_tx.clone(); + let stderr_tail = Arc::new(Mutex::new(String::new())); + let captured = stderr_tail.clone(); + std::thread::spawn(move || { + for line in BufReader::new(stderr).lines().map_while(Result::ok) { + let mut tail = captured.lock().unwrap_or_else(|e| e.into_inner()); + if tail.len() + line.len() > 32_768 { + tail.clear(); + } + tail.push_str(&line); + tail.push('\n'); + drop(tail); + if err_tx + .send(SessionEvent::Output( + format!("[codex] {line}\r\n").into_bytes(), + )) + .is_err() + { + break; + } + } + }); + let (ready_tx, ready_rx) = mpsc::sync_channel(1); + let spec = spec.clone(); + let worker_child = child.clone(); + std::thread::spawn(move || worker::run(spec, stdin, rx, events_tx, ready_tx, worker_child)); + let ready = ready_rx + .recv_timeout(Duration::from_secs(30)) + .context("Codex initialization timed out"); + match ready { + Ok(Ok(())) => Ok(StartedSession { + session: Box::new(CodexSession { tx, child }), + events: events_rx, + msg_socket: None, + }), + outcome => { + let _ = child.lock().unwrap_or_else(|e| e.into_inner()).kill(); + let detail = stderr_tail + .lock() + .unwrap_or_else(|e| e.into_inner()) + .clone(); + match outcome { + Ok(Err(error)) | Err(error) => { + if detail.trim().is_empty() { + Err(error) + } else { + Err(error).context(detail.trim().to_string()) + } + } + Ok(Ok(())) => unreachable!(), + } + } + } + } +} + +struct CodexSession { + tx: mpsc::Sender, + child: Arc>, +} + +impl RuntimeSession for CodexSession { + fn send_input(&mut self, bytes: &[u8]) -> anyhow::Result<()> { + self.tx + .send(Wire::Input(bytes.to_vec())) + .map_err(|_| anyhow::anyhow!("Codex session stopped")) + } + fn deliver(&mut self, text: &str) -> Option> { + let (reply_tx, reply_rx) = mpsc::sync_channel(1); + Some((|| { + self.tx + .send(Wire::Deliver(text.to_string(), reply_tx)) + .map_err(|_| anyhow::anyhow!("Codex session stopped"))?; + reply_rx + .recv_timeout(Duration::from_secs(12)) + .context("Codex delivery timed out")? + })()) + } + fn resize(&mut self, _cols: u16, _rows: u16) -> anyhow::Result<()> { + Ok(()) + } + fn kill(&mut self) -> anyhow::Result<()> { + let mut child = self.child.lock().unwrap_or_else(|e| e.into_inner()); + if child.try_wait()?.is_none() { + child.kill()?; + } + let _ = self.tx.send(Wire::Stop); + Ok(()) + } +} + +impl Drop for CodexSession { + fn drop(&mut self) { + let _ = self.kill(); + } +} diff --git a/crates/gravityd/src/runtime/codex/approvals.rs b/crates/gravityd/src/runtime/codex/approvals.rs new file mode 100644 index 0000000..14957ef --- /dev/null +++ b/crates/gravityd/src/runtime/codex/approvals.rs @@ -0,0 +1,128 @@ +use super::worker::Worker; +use serde_json::{json, Value}; + +pub(super) struct Prompt { + pub id: Value, + pub method: String, + pub params: Value, +} + +impl Worker { + pub fn prompt(&mut self, id: Value, method: &str, params: Value) -> anyhow::Result<()> { + if !matches!( + method, + "item/commandExecution/requestApproval" + | "item/fileChange/requestApproval" + | "item/permissions/requestApproval" + | "item/tool/requestUserInput" + | "tool/requestUserInput" + | "mcpServer/elicitation/request" + ) { + return self.write(&json!({ "id": id, "error": { "code": -32601, "message": "Gravity does not support this server request" } })); + } + self.prompt_serial += 1; + let number = self.prompt_serial; + let detail = serde_json::to_string_pretty(¶ms)?; + self.output(&format!("\nRequest #{number}:\n{detail}\nUse /approve {number} or /deny {number}. For questions use /answer {number} .\n")); + self.hook( + "Notification", + Some(format!( + "Codex request #{number} needs your response in the terminal" + )), + ); + self.prompts.insert( + number, + Prompt { + id, + method: method.to_string(), + params, + }, + ); + Ok(()) + } + pub fn answer(&mut self, text: &str) -> anyhow::Result { + let mut parts = text.splitn(3, ' '); + let command = parts.next().unwrap_or_default(); + if !matches!(command, "/approve" | "/deny" | "/answer") { + return Ok(false); + } + let number: u64 = parts + .next() + .ok_or_else(|| anyhow::anyhow!("include the request number"))? + .parse()?; + let prompt = self + .prompts + .get(&number) + .ok_or_else(|| anyhow::anyhow!("request #{number} is no longer pending"))?; + let result = if command == "/answer" { + let response: Value = serde_json::from_str( + parts + .next() + .ok_or_else(|| anyhow::anyhow!("include the JSON response"))?, + )?; + // Only question/elicitation answers accept structured payloads. + anyhow::ensure!( + matches!( + prompt.method.as_str(), + "item/tool/requestUserInput" + | "tool/requestUserInput" + | "mcpServer/elicitation/request" + ), + "use /approve or /deny for this request" + ); + validate_answer(&prompt.method, &response)?; + response + } else { + let accept = command == "/approve"; + match prompt.method.as_str() { + "item/commandExecution/requestApproval" | "item/fileChange/requestApproval" => { + json!({ "decision": if accept { "accept" } else { "decline" } }) + } + "item/permissions/requestApproval" => { + json!({ "permissions": if accept { prompt.params.get("permissions").cloned().unwrap_or(json!({})) } else { json!({}) }, "scope": "turn" }) + } + "mcpServer/elicitation/request" if !accept => { + json!({ "action": "decline", "content": null }) + } + _ => anyhow::bail!("answer the questions with /answer {number} "), + } + }; + self.write(&json!({ "id": prompt.id, "result": result }))?; + self.prompts.remove(&number); + self.hook("PostToolUse", None); + Ok(true) + } +} + +fn validate_answer(method: &str, response: &Value) -> anyhow::Result<()> { + if method == "mcpServer/elicitation/request" { + anyhow::ensure!( + matches!( + response.get("action").and_then(Value::as_str), + Some("accept" | "decline" | "cancel") + ), + "answer needs action: accept, decline or cancel" + ); + anyhow::ensure!( + response + .get("content") + .is_some_and(|v| v.is_null() || v.is_object()), + "answer needs content: an object or null" + ); + } else { + let answers = response + .get("answers") + .and_then(Value::as_object) + .ok_or_else(|| { + anyhow::anyhow!("answer needs an answers object keyed by question ID") + })?; + anyhow::ensure!( + answers.values().all(|answer| answer + .get("answers") + .and_then(Value::as_array) + .is_some_and(|items| items.iter().all(Value::is_string))), + "each question answer needs an answers array of strings" + ); + } + Ok(()) +} diff --git a/crates/gravityd/src/runtime/codex/observations.rs b/crates/gravityd/src/runtime/codex/observations.rs new file mode 100644 index 0000000..3279b47 --- /dev/null +++ b/crates/gravityd/src/runtime/codex/observations.rs @@ -0,0 +1,11 @@ +use serde_json::json; +use std::fs::OpenOptions; +use std::io::Write; +use std::path::Path; + +pub(super) fn append(path: &Path, role: &str, text: &str) -> anyhow::Result<()> { + let mut file = OpenOptions::new().create(true).append(true).open(path)?; + let entry = json!({ "type": role, "timestamp": chrono::Utc::now().to_rfc3339(), "message": { "content": [{ "type": "text", "text": text }] } }); + writeln!(file, "{entry}")?; + Ok(()) +} diff --git a/crates/gravityd/src/runtime/codex/worker.rs b/crates/gravityd/src/runtime/codex/worker.rs new file mode 100644 index 0000000..d2ffe0c --- /dev/null +++ b/crates/gravityd/src/runtime/codex/worker.rs @@ -0,0 +1,369 @@ +use super::{approvals::Prompt, observations, transcript_path, BotSpec, SessionEvent, Wire}; +use anyhow::Context; +use serde_json::{json, Value}; +use std::collections::{BTreeMap, VecDeque}; +use std::io::{BufWriter, Write}; +use std::path::PathBuf; +use std::process::{Child, ChildStdin}; +use std::sync::{mpsc, Arc, Mutex}; +use std::time::{Duration, Instant}; + +pub(super) struct Worker { + pub spec: BotSpec, + pub input: BufWriter, + pub rx: mpsc::Receiver, + pub events: tokio::sync::mpsc::UnboundedSender, + pub queued: VecDeque, + pub serial: u64, + pub thread: String, + pub turn: Option, + pub line: Vec, + pub escaping: bool, + pub prompts: BTreeMap, + pub prompt_serial: u64, + pub transcript: PathBuf, + pub dead: bool, + pub completed_turns: u64, +} + +pub(super) fn run( + spec: BotSpec, + stdin: ChildStdin, + rx: mpsc::Receiver, + events: tokio::sync::mpsc::UnboundedSender, + ready: mpsc::SyncSender>, + child: Arc>, +) { + let transcript = transcript_path(&spec.workspace); + let mut worker = Worker { + spec, + input: BufWriter::new(stdin), + rx, + events, + queued: VecDeque::new(), + serial: 0, + thread: String::new(), + turn: None, + line: Vec::new(), + escaping: false, + prompts: BTreeMap::new(), + prompt_serial: 0, + transcript, + dead: false, + completed_turns: 0, + }; + let result = worker.initialize(); + match result { + Ok(()) => { + if ready.send(Ok(())).is_ok() { + worker.main_loop(); + } + } + Err(error) => { + let _ = ready.send(Err(error)); + } + } + let code = { + let mut child = child.lock().unwrap_or_else(|e| e.into_inner()); + let _ = child.kill(); + child.wait().ok().and_then(|status| status.code()) + }; + let _ = worker.events.send(SessionEvent::Exited { code }); +} + +impl Worker { + pub fn output(&self, text: &str) { + let _ = self.events.send(SessionEvent::Output( + text.replace('\n', "\r\n").into_bytes(), + )); + } + pub fn hook(&self, event: &'static str, detail: Option) { + let transcript = (event == "Stop").then(|| self.transcript.display().to_string()); + let _ = self.events.send(SessionEvent::Lifecycle { + event, + detail, + transcript, + }); + } + pub fn write(&mut self, value: &Value) -> anyhow::Result<()> { + serde_json::to_writer(&mut self.input, value)?; + self.input.write_all(b"\n")?; + self.input.flush()?; + Ok(()) + } + fn rpc(&mut self, method: &str, params: Value) -> anyhow::Result { + self.serial += 1; + let id = self.serial; + self.write(&json!({ "id": id, "method": method, "params": params }))?; + let until = Instant::now() + Duration::from_secs(10); + loop { + let message = match self + .rx + .recv_timeout(until.saturating_duration_since(Instant::now())) + { + Ok(message) => message, + Err(error) => { + self.dead = true; + return Err(error) + .with_context(|| format!("Codex {method} response timed out")); + } + }; + match message { + Wire::Server(value) + if value.get("id") == Some(&json!(id)) && value.get("method").is_none() => + { + if let Some(error) = value.get("error") { + anyhow::bail!( + "Codex {method}: {}", + error + .get("message") + .and_then(Value::as_str) + .unwrap_or("request failed") + ); + } + return value + .get("result") + .cloned() + .context("missing Codex RPC result"); + } + Wire::Server(value) => self.server(value)?, + Wire::Closed | Wire::Stop => { + self.dead = true; + anyhow::bail!("Codex process stopped"); + } + command => self.queued.push_back(command), + } + } + } + fn initialize(&mut self) -> anyhow::Result<()> { + self.rpc("initialize", json!({ "clientInfo": { "name": "gravity", "title": "Gravity", "version": env!("CARGO_PKG_VERSION") } }))?; + self.write(&json!({ "method": "initialized", "params": {} }))?; + let codex = self.spec.codex.as_ref().context("missing Codex settings")?; + let mut roots = vec![self.spec.workspace.display().to_string()]; + if let Some(artifacts) = &codex.artifacts { + roots.push(artifacts.display().to_string()); + } + let mut params = json!({ + "cwd": self.spec.workspace, + "approvalPolicy": "on-request", "sandbox": "workspace-write", + "developerInstructions": self.spec.workspace.parent().and_then(|root| std::fs::read_to_string(root.join("system.md")).ok()).unwrap_or_default() + + "\nRead CLAUDE.md and FACTS.md for your saved context. Keep durable facts in FACTS.md.", + "config": { + "mcp_servers.gravity-bus": { "url": format!("http://127.0.0.1:{}/mcp", codex.port), "bearer_token_env_var": "GRAVITY_TOKEN" }, + "sandbox_workspace_write.writable_roots": roots + } + }); + let path = self + .spec + .workspace + .parent() + .unwrap_or(&self.spec.workspace) + .join("codex-thread-id"); + let saved = std::fs::read_to_string(&path) + .ok() + .map(|id| id.trim().to_string()) + .filter(|id| !id.is_empty()); + let method = if let Some(id) = saved { + params["threadId"] = json!(id); + "thread/resume" + } else { + "thread/start" + }; + let reply = match self.rpc(method, params.clone()) { + Err(error) + if method == "thread/resume" + && error.to_string().contains("no rollout found for thread id") + && std::fs::metadata(&self.transcript).map_or(true, |m| m.len() == 0) => + { + // App Server creates a rollout only after the first turn. + if let Some(object) = params.as_object_mut() { + object.remove("threadId"); + } + self.rpc("thread/start", params)? + } + result => result?, + }; + self.thread = reply + .pointer("/thread/id") + .and_then(Value::as_str) + .context("Codex did not return a thread ID")? + .to_string(); + std::fs::write(path, format!("{}\n", self.thread))?; + self.output("Codex CLI ready. Type a prompt and press Enter. /interrupt stops a turn.\n"); + self.hook("SessionStart", None); + Ok(()) + } + fn main_loop(&mut self) { + while let Some(message) = self.queued.pop_front().or_else(|| self.rx.recv().ok()) { + let result = match message { + Wire::Server(value) => self.server(value), + Wire::Input(bytes) => self.keystrokes(&bytes), + Wire::Deliver(text, reply) => { + let result = self.deliver(&text); + let _ = reply.send(result); + Ok(()) + } + Wire::Closed | Wire::Stop => break, + }; + if let Err(error) = result { + self.output(&format!("\n[Codex error] {error}\n")); + } + if self.dead { + break; + } + } + } + fn deliver(&mut self, text: &str) -> anyhow::Result<()> { + observations::append(&self.transcript, "user", text)?; + let input = json!([{ "type": "text", "text": text }]); + if let Some(turn) = &self.turn { + match self.rpc( + "turn/steer", + json!({ "threadId": self.thread, "expectedTurnId": turn, "input": input }), + ) { + Ok(_) => return Ok(()), + Err(error) if self.dead || self.turn.is_some() => return Err(error), + Err(_) => {} // The completed notification raced the steering request. + } + } + { + let completed = self.completed_turns; + let reply = self.rpc( + "turn/start", + json!({ "threadId": self.thread, "input": input }), + )?; + // A completed notification can precede the reply for a fast turn. + if self.completed_turns == completed + && reply.pointer("/turn/status").and_then(Value::as_str) == Some("inProgress") + { + self.turn = reply + .pointer("/turn/id") + .and_then(Value::as_str) + .map(str::to_string); + } + } + Ok(()) + } + fn keystrokes(&mut self, bytes: &[u8]) -> anyhow::Result<()> { + for &byte in bytes { + if self.escaping { + if byte != b'[' && (0x40..=0x7e).contains(&byte) { + self.escaping = false; + } + continue; + } + match byte { + 27 => self.escaping = true, + 3 => { + self.line.clear(); + self.interrupt()?; + } + 8 | 127 => { + if let Some(last) = self.line.pop() { + if last & 0xc0 == 0x80 { + while self.line.pop().is_some_and(|b| b & 0xc0 == 0x80) {} + } + self.output("\u{8} \u{8}"); + } + } + 13 | 10 if !self.line.is_empty() => { + let text = String::from_utf8(std::mem::take(&mut self.line)) + .context("prompt is not UTF-8")?; + self.output("\n"); + if text == "/interrupt" { + self.interrupt()?; + } else if !self.answer(&text)? { + self.deliver(&text)?; + } + } + 13 | 10 => {} + b if b >= 32 => self.line.push(b), + _ => {} + } + } + // Echo whole UTF-8 inputs rather than one byte per output frame. + if !bytes + .iter() + .any(|b| matches!(b, 3 | 8 | 10 | 13 | 27 | 127)) + { + let _ = self.events.send(SessionEvent::Output(bytes.to_vec())); + } + Ok(()) + } + fn interrupt(&mut self) -> anyhow::Result<()> { + if let Some(turn) = &self.turn { + self.rpc( + "turn/interrupt", + json!({ "threadId": self.thread, "turnId": turn }), + )?; + } + Ok(()) + } + fn server(&mut self, value: Value) -> anyhow::Result<()> { + let Some(method) = value.get("method").and_then(Value::as_str) else { + return Ok(()); + }; + if let Some(id) = value.get("id") { + return self.prompt( + id.clone(), + method, + value.get("params").cloned().unwrap_or(Value::Null), + ); + } + let params = value.get("params").unwrap_or(&Value::Null); + match method { + "turn/started" => { + self.turn = params + .pointer("/turn/id") + .and_then(Value::as_str) + .map(str::to_string); + self.hook("UserPromptSubmit", None); + } + "turn/completed" => { + self.completed_turns += 1; + self.turn = None; + self.output("\n"); + if let Some(error) = params + .pointer("/turn/error/message") + .and_then(Value::as_str) + { + self.output(error); + } + if params.pointer("/turn/status").and_then(Value::as_str) == Some("completed") { + self.hook("Stop", None); + } else { + self.hook("TurnInterrupted", None); + } + } + "item/agentMessage/delta" | "item/commandExecution/outputDelta" => { + if let Some(delta) = params.get("delta").and_then(Value::as_str) { + self.output(delta); + } + } + "item/started" => { + if let Some(command) = params.pointer("/item/command").and_then(Value::as_str) { + self.output(&format!("\n$ {command}\n")); + } + } + "item/completed" => { + if params.pointer("/item/type").and_then(Value::as_str) == Some("agentMessage") { + if let Some(text) = params.pointer("/item/text").and_then(Value::as_str) { + observations::append(&self.transcript, "assistant", text)?; + } + } + self.hook("PostToolUse", None); + } + "serverRequest/resolved" => { + let request = params.get("requestId"); + self.prompts.retain(|_, prompt| Some(&prompt.id) != request); + } + "error" => { + if let Some(message) = params.pointer("/error/message").and_then(Value::as_str) { + self.output(&format!("\n[Codex] {message}\n")); + } + } + _ => {} + } + Ok(()) + } +} diff --git a/crates/gravityd/src/runtime/mixed.rs b/crates/gravityd/src/runtime/mixed.rs new file mode 100644 index 0000000..6671edf --- /dev/null +++ b/crates/gravityd/src/runtime/mixed.rs @@ -0,0 +1,51 @@ +use super::codex::CodexAdapter; +use super::pty::PtyAdapter; +use super::{BotSpec, Capabilities, RuntimeAdapter, StartedSession}; +use crate::config::Config; + +pub struct MixedAdapter { + claude_bin: String, + codex_bin: String, +} + +impl MixedAdapter { + pub fn new(cfg: &Config) -> Self { + Self { + claude_bin: cfg.claude_bin.clone(), + codex_bin: cfg.codex_bin.clone(), + } + } +} + +impl RuntimeAdapter for MixedAdapter { + fn capabilities(&self) -> Capabilities { + Capabilities { + kind: "cli", + native_background: false, + channel_delivery: true, + permission_relay: true, + } + } + fn start(&self, spec: &BotSpec) -> anyhow::Result { + if spec.codex.is_some() { + CodexAdapter.start(spec) + } else { + PtyAdapter.start(spec) + } + } + fn probe(&self) -> anyhow::Result { + let versions: Vec = [&self.claude_bin, &self.codex_bin] + .into_iter() + .filter_map(|bin| { + std::process::Command::new(bin) + .arg("--version") + .output() + .ok() + .filter(|out| out.status.success()) + .map(|out| String::from_utf8_lossy(&out.stdout).trim().to_string()) + }) + .collect(); + anyhow::ensure!(!versions.is_empty(), "neither configured CLI is available"); + Ok(versions.join("; ")) + } +} diff --git a/crates/gravityd/src/runtime/mod.rs b/crates/gravityd/src/runtime/mod.rs index 0beddcc..a7ddac1 100644 --- a/crates/gravityd/src/runtime/mod.rs +++ b/crates/gravityd/src/runtime/mod.rs @@ -4,9 +4,11 @@ //! Bus deliveries never go through the PTY: they are posted to the session's //! inbox socket (see `channel`). The PTY carries only terminal I/O. +pub mod codex; pub mod double; #[cfg(windows)] mod inbox; +pub mod mixed; pub mod pty; use std::path::PathBuf; @@ -30,6 +32,7 @@ pub struct Capabilities { /// Everything an adapter needs to start one bot session. #[derive(Debug, Clone)] pub struct BotSpec { + pub codex: Option, pub bot_id: String, pub bot_name: String, pub workspace: PathBuf, @@ -44,7 +47,14 @@ pub struct BotSpec { #[derive(Debug)] pub enum SessionEvent { Output(Vec), - Exited { code: Option }, + Exited { + code: Option, + }, + Lifecycle { + event: &'static str, + detail: Option, + transcript: Option, + }, } /// A freshly started runtime session. @@ -59,6 +69,10 @@ pub struct StartedSession { /// A live runtime session for one bot. Terminal I/O only — deliveries go /// through the inbox socket, never through here. pub trait RuntimeSession: Send { + /// A structured transport, when supported. Never writes terminal input. + fn deliver(&mut self, _text: &str) -> Option> { + None + } /// Raw user keystrokes forwarded from the attached terminal. fn send_input(&mut self, bytes: &[u8]) -> anyhow::Result<()>; fn resize(&mut self, cols: u16, rows: u16) -> anyhow::Result<()>; diff --git a/crates/gravityd/src/runtime/pty/tests.rs b/crates/gravityd/src/runtime/pty/tests.rs index 6976d19..3188847 100644 --- a/crates/gravityd/src/runtime/pty/tests.rs +++ b/crates/gravityd/src/runtime/pty/tests.rs @@ -109,6 +109,7 @@ async fn a_real_pty_burst_is_merged_into_few_frames() { "i=0; while [ $i -lt {WRITES} ]; do printf '\\033[2K\\033[1A%04d' $i; i=$((i+1)); done" ); let spec = BotSpec { + codex: None, bot_id: "pty-test".into(), bot_name: "pty".into(), workspace: std::env::temp_dir(), @@ -128,6 +129,7 @@ async fn a_real_pty_burst_is_merged_into_few_frames() { bytes += data.len(); } SessionEvent::Exited { .. } => break, + SessionEvent::Lifecycle { .. } => {} } } // Each write is two 4-byte escape sequences plus 4 digits. diff --git a/crates/gravityd/src/supervisor.rs b/crates/gravityd/src/supervisor.rs index 01d131c..3c003fe 100644 --- a/crates/gravityd/src/supervisor.rs +++ b/crates/gravityd/src/supervisor.rs @@ -31,6 +31,7 @@ use crate::terminal::TermBuffer; mod hooks; mod lifecycle; +mod restart; mod termio; pub const BOT_TOKEN_ENV: &str = "GRAVITY_TOKEN"; @@ -113,6 +114,7 @@ struct BotHandle { /// Set when the daemon asked for the stop (archival), so exit is not a /// crash and the reconciler leaves the bot alone. stopping: bool, + restart_pending: bool, consecutive_crashes: u32, last_start: Instant, /// Earliest time the reconciler may start this bot again; set while a @@ -141,6 +143,7 @@ impl BotHandle { repaint_restore: None, repaint_generation: 0, stopping: false, + restart_pending: false, consecutive_crashes: 0, last_start: Instant::now(), next_start_at: None, @@ -273,7 +276,7 @@ impl Supervisor { /// message is read between tool calls or starts a new turn when the /// session is idle; it never touches the terminal. pub fn deliver(&self, bot_id: &str, text: &str) -> Result<(), DeliverError> { - let socket = { + let (session, socket) = { let bots = self.lock_bots(); let Some(h) = bots.get(bot_id) else { return Err(DeliverError::NotReady("bot has no runtime".to_string())); @@ -284,15 +287,19 @@ impl Supervisor { h.state.as_str() ))); } - match &h.msg_socket { - Some(s) => s.clone(), - None => { - return Err(DeliverError::NotReady( - "inbox socket not reported yet".to_string(), - )); - } - } + (h.session.clone(), h.msg_socket.clone()) }; + if let Some(session) = session { + if let Some(result) = session + .lock() + .unwrap_or_else(|e| e.into_inner()) + .deliver(text) + { + return result.map_err(DeliverError::Failed); + } + } + let socket = socket + .ok_or_else(|| DeliverError::NotReady("inbox socket not reported yet".to_string()))?; crate::channel::send(&socket, text).map_err(DeliverError::Failed) } } diff --git a/crates/gravityd/src/supervisor/hooks.rs b/crates/gravityd/src/supervisor/hooks.rs index 764f662..eabd9bf 100644 --- a/crates/gravityd/src/supervisor/hooks.rs +++ b/crates/gravityd/src/supervisor/hooks.rs @@ -29,6 +29,9 @@ impl Supervisor { } } "UserPromptSubmit" => self.set_state(bot_id, BotState::Working, "prompt submitted"), + "TurnInterrupted" => { + self.set_state(bot_id, BotState::Ready, "turn interrupted or failed") + } // Answering a permission prompt fires no hook of its own, so a bot // that was approved and carried on used to sit on "waiting for // approval" until its turn ended. A completed tool call is the diff --git a/crates/gravityd/src/supervisor/lifecycle.rs b/crates/gravityd/src/supervisor/lifecycle.rs index be8c616..3fb3a8c 100644 --- a/crates/gravityd/src/supervisor/lifecycle.rs +++ b/crates/gravityd/src/supervisor/lifecycle.rs @@ -39,7 +39,9 @@ impl Supervisor { // provisioned before trust marking existed (or whose entry in // `~/.claude.json` was lost) would otherwise greet every daemon // restart with Claude Code's trust dialog. - if self.inner.cfg.runtime == crate::config::RuntimeKind::Pty { + if self.inner.cfg.runtime == crate::config::RuntimeKind::Pty + && bot.runtime == bus::BotRuntime::ClaudeCode + { if let Err(e) = crate::paths::trust_workspace(&self.inner.cfg.user_home, &workspace) { tracing::warn!(bot_id, error = %e, "could not trust bot workspace"); @@ -64,13 +66,17 @@ impl Supervisor { // A bot is one continuous conversation: every start after the first // resumes the workspace's session, so a daemon restart is invisible to // the bot and to whoever was talking to it. - let resume = self.wants_resume(bot_id); + let resume = bot.runtime == bus::BotRuntime::ClaudeCode && self.wants_resume(bot_id); let mut claude_args = self.inner.cfg.claude_args.clone(); if resume { claude_args.push("--continue".to_string()); } // Whatever model the bot was last talking with, it keeps talking with. - let model = self.pinned_model(bot_id, &workspace); + let model = if bot.runtime == bus::BotRuntime::ClaudeCode { + self.pinned_model(bot_id, &workspace) + } else { + None + }; if let Some(model) = &model { claude_args.push("--model".to_string()); claude_args.push(model.clone()); @@ -120,6 +126,14 @@ impl Supervisor { } let spec = BotSpec { + codex: (bot.runtime == bus::BotRuntime::CodexCli).then(|| { + crate::runtime::codex::CodexSpec { + bin: self.inner.cfg.codex_bin.clone(), + args: self.inner.cfg.codex_args.clone(), + port: self.inner.cfg.port, + artifacts, + } + }), bot_id: bot.id.clone(), bot_name: bot.name.clone(), workspace, @@ -146,6 +160,7 @@ impl Supervisor { handle.msg_socket = started.msg_socket; handle.size = (spec.cols, spec.rows); handle.stopping = false; + handle.restart_pending = false; handle.next_start_at = None; handle.resumed = resume; handle.pinned_model = model.is_some(); @@ -185,6 +200,18 @@ impl Supervisor { sup.on_exit(&bot_id_owned, code); break; } + SessionEvent::Lifecycle { + event, + detail, + transcript, + } => { + sup.on_hook_with_transcript( + &bot_id_owned, + event, + detail.as_deref(), + transcript.as_deref(), + ); + } } } }); @@ -329,6 +356,10 @@ impl Supervisor { } else { h.consecutive_crashes = 0; h.next_start_at = None; + if h.restart_pending { + h.stopping = false; + h.restart_pending = false; + } } (was_stopping, h.consecutive_crashes) }; diff --git a/crates/gravityd/src/supervisor/restart.rs b/crates/gravityd/src/supervisor/restart.rs new file mode 100644 index 0000000..247ff58 --- /dev/null +++ b/crates/gravityd/src/supervisor/restart.rs @@ -0,0 +1,30 @@ +use super::*; + +impl Supervisor { + pub fn restart_bot(&self, bot_id: &str) -> anyhow::Result<()> { + let previous = self.state(bot_id); + let session = { + let mut bots = self.lock_bots(); + let Some(handle) = bots.get_mut(bot_id) else { + return Ok(()); + }; + handle.restart_pending = handle.session.is_some(); + handle.stopping = handle.session.is_some(); + handle.next_start_at = None; + handle.session.clone() + }; + if let Some(session) = session { + self.set_state(bot_id, BotState::Stopping, "runtime changed"); + let result = session.lock().unwrap_or_else(|e| e.into_inner()).kill(); + if let Err(error) = result { + if let Some(handle) = self.lock_bots().get_mut(bot_id) { + handle.stopping = false; + handle.restart_pending = false; + } + self.set_state(bot_id, previous.0, &previous.1); + return Err(error); + } + } + Ok(()) + } +} diff --git a/crates/gravityd/src/ws.rs b/crates/gravityd/src/ws.rs index 5d2e8c6..72c0b7d 100644 --- a/crates/gravityd/src/ws.rs +++ b/crates/gravityd/src/ws.rs @@ -24,6 +24,7 @@ mod dispatch; mod entities; mod messaging; mod routines; +mod runtime; mod terminal; mod views; diff --git a/crates/gravityd/src/ws/dispatch.rs b/crates/gravityd/src/ws/dispatch.rs index b842860..658f2e0 100644 --- a/crates/gravityd/src/ws/dispatch.rs +++ b/crates/gravityd/src/ws/dispatch.rs @@ -82,6 +82,7 @@ impl Conn { "list_bot_activity" => self.list_bot_activity(&req_id, req), "create_bot" => self.create_bot(&req_id, req), "update_bot" => self.update_bot(&req_id, req), + "set_bot_runtime" => self.set_bot_runtime(&req_id, req), "delete_bot" => self.delete_bot(&req_id, req), "list_bot_revisions" => self.list_bot_revisions(&req_id, req), "revert_bot_revision" => self.revert_bot_revision(&req_id, req), diff --git a/crates/gravityd/src/ws/entities.rs b/crates/gravityd/src/ws/entities.rs index c2920c7..a82964d 100644 --- a/crates/gravityd/src/ws/entities.rs +++ b/crates/gravityd/src/ws/entities.rs @@ -155,6 +155,8 @@ impl Conn { pub(super) fn create_bot(&self, req_id: &Value, req: &Value) -> anyhow::Result<()> { let project_id = Self::str_field(req, "project_id")?; + let runtime = + super::runtime::requested_runtime(req)?.unwrap_or(self.app.cfg.default_bot_runtime); // An explicit `null` is how a client with no name to give says so, so // it takes the placeholder path rather than failing as a bad string. let supplied = match req.get("name") { @@ -187,7 +189,14 @@ impl Conn { instructions: req.get("instructions").and_then(|v| v.as_str()), avatar: req.get("avatar").and_then(|v| v.as_str()), }; - match botmgmt::create_bot(&self.app, project_id, &edit, None, &Actor::User) { + match botmgmt::create_bot_with_runtime( + &self.app, + project_id, + &edit, + None, + &Actor::User, + runtime, + ) { Ok(created) => { self.send(json!({ "type": "bot", "req_id": req_id, diff --git a/crates/gravityd/src/ws/runtime.rs b/crates/gravityd/src/ws/runtime.rs new file mode 100644 index 0000000..188e395 --- /dev/null +++ b/crates/gravityd/src/ws/runtime.rs @@ -0,0 +1,43 @@ +use super::Conn; +use crate::events::Push; +use bus::BotRuntime; +use serde_json::{json, Value}; + +pub(super) fn requested_runtime(req: &Value) -> anyhow::Result> { + req.get("runtime") + .map(|value| serde_json::from_value(value.clone()).map_err(anyhow::Error::from)) + .transpose() +} + +impl Conn { + pub(super) fn set_bot_runtime(&self, req_id: &Value, req: &Value) -> anyhow::Result<()> { + let bot_id = Self::str_field(req, "bot_id")?; + let runtime = match requested_runtime(req) { + Ok(Some(runtime)) => runtime, + result => { + let message = result + .err() + .map(|error| error.to_string()) + .unwrap_or_else(|| "runtime is required".to_string()); + self.reply_err(req_id, "invalid_request", &message); + return Ok(()); + } + }; + let mut bot = self + .app + .db + .get_live_bot(bot_id)? + .ok_or_else(|| anyhow::anyhow!("bot not found"))?; + if bot.runtime != runtime { + self.app.db.set_bot_runtime(bot_id, runtime)?; + if let Err(error) = self.app.supervisor.restart_bot(bot_id) { + self.app.db.set_bot_runtime(bot_id, bot.runtime)?; + return Err(error); + } + bot.runtime = runtime; + self.app.events.push(Push::BotUpdated { bot: bot.clone() }); + } + self.send(json!({ "type": "bot", "req_id": req_id, "bot": self.bot_json(&bot) })); + Ok(()) + } +} diff --git a/crates/gravityd/src/ws/views.rs b/crates/gravityd/src/ws/views.rs index d0dd941..612d5af 100644 --- a/crates/gravityd/src/ws/views.rs +++ b/crates/gravityd/src/ws/views.rs @@ -35,6 +35,7 @@ pub(crate) fn bot_view(app: &AppState, bot: &bus::Bot) -> Value { "description": bot.description, "avatar": bot.avatar, "instructions": bot.instructions, + "runtime": bot.runtime, "state": state.as_str(), "state_reason": reason, "unread_count": unread, diff --git a/crates/gravityd/tests/bot_runtime.rs b/crates/gravityd/tests/bot_runtime.rs new file mode 100644 index 0000000..5ec61ea --- /dev/null +++ b/crates/gravityd/tests/bot_runtime.rs @@ -0,0 +1,80 @@ +mod common; +mod legacy; + +use bus::BotRuntime; +use common::{spawn_daemon, spawn_daemon_with, WsClient}; +use gravityd::db::Db; +use serde_json::json; + +#[test] +fn migration_defaults_existing_bots_and_persists_runtime() { + let dir = tempfile::tempdir().expect("tempdir"); + let path = dir.path().join("bus.sqlite"); + let id = legacy::v2_database(&path, "/tmp/bot/workspace"); + let db = Db::open(&path).expect("migrate"); + assert_eq!( + db.get_bot(&id).unwrap().unwrap().runtime, + BotRuntime::ClaudeCode + ); + db.set_bot_runtime(&id, BotRuntime::CodexCli).unwrap(); + drop(db); + let db = Db::open(&path).unwrap(); + assert_eq!( + db.get_bot(&id).unwrap().unwrap().runtime, + BotRuntime::CodexCli + ); + assert!(db.set_bot_runtime("missing", BotRuntime::CodexCli).is_err()); +} + +#[tokio::test] +async fn runtime_can_be_created_changed_and_restarted_without_moving_workspace() { + let d = spawn_daemon().await; + let mut c = WsClient::connect(&d).await; + let p = c + .request(json!({"type":"create_project", "name":"Runtime"})) + .await; + let b = c.request(json!({"type":"create_bot", "project_id":p["project"]["id"], "name":"Tester", "runtime":"codex_cli"})).await; + assert_eq!(b["bot"]["runtime"], "codex_cli", "{b}"); + let id = b["bot"]["id"].as_str().unwrap(); + let reply = c + .request(json!({"type":"set_bot_runtime", "bot_id":id, "runtime":"claude_code"})) + .await; + assert_eq!(reply["bot"]["runtime"], "claude_code", "{reply}"); + assert_eq!(reply["bot"]["workspace_path"], b["bot"]["workspace_path"]); + tokio::time::timeout(std::time::Duration::from_secs(6), async { + loop { + if d.app.supervisor.active_total() == 0 { + break; + } + tokio::time::sleep(std::time::Duration::from_millis(20)).await; + } + }) + .await + .unwrap(); + d.app.supervisor.reconcile(); + assert_eq!(d.app.supervisor.active_total(), 1); + let invalid = c + .request(json!({"type":"set_bot_runtime", "bot_id":id, "runtime":"unknown"})) + .await; + assert_eq!(invalid["type"], "error"); + assert_eq!( + d.app.db.get_bot(id).unwrap().unwrap().runtime, + BotRuntime::ClaudeCode + ); + let no_op = c + .request(json!({"type":"set_bot_runtime", "bot_id":id, "runtime":"claude_code"})) + .await; + assert_eq!(no_op["type"], "bot"); + assert_eq!(d.app.supervisor.active_total(), 1); +} + +#[tokio::test] +async fn omitted_runtime_uses_configured_default() { + let d = spawn_daemon_with(|cfg| cfg.default_bot_runtime = BotRuntime::CodexCli).await; + let mut c = WsClient::connect(&d).await; + let p = c + .request(json!({"type":"create_project", "name":"Default"})) + .await; + let b = common::create_bot(&mut c, p["project"]["id"].as_str().unwrap(), "Default").await; + assert_eq!(b["runtime"], "codex_cli"); +} diff --git a/crates/gravityd/tests/codex_native.rs b/crates/gravityd/tests/codex_native.rs new file mode 100644 index 0000000..616b9e7 --- /dev/null +++ b/crates/gravityd/tests/codex_native.rs @@ -0,0 +1,40 @@ +//! Opt-in handshake against a locally installed Codex CLI; no model turn is sent. +use gravityd::runtime::codex::{CodexAdapter, CodexSpec}; +use gravityd::runtime::{BotSpec, RuntimeAdapter}; + +#[test] +#[ignore = "requires CODEX_BINARY pointing to a locally installed CLI"] +fn native_codex_initializes_and_resumes_without_a_model_request() { + let root = tempfile::tempdir().unwrap(); + let workspace = root.path().join("workspace"); + std::fs::create_dir_all(&workspace).unwrap(); + std::fs::create_dir_all(root.path().join("codex-home")).unwrap(); + let spec = BotSpec { + bot_id: "native-smoke".into(), + bot_name: "Native smoke".into(), + workspace, + claude_bin: "unused".into(), + claude_args: Vec::new(), + codex: Some(CodexSpec { + bin: std::env::var("CODEX_BINARY").expect("CODEX_BINARY"), + args: Vec::new(), + port: 1, + artifacts: None, + }), + env: vec![ + ("GRAVITY_TOKEN".into(), "smoke-not-a-real-token".into()), + ( + "CODEX_HOME".into(), + root.path().join("codex-home").display().to_string(), + ), + ], + cols: 80, + rows: 24, + }; + let mut started = CodexAdapter.start(&spec).expect("native initialization"); + assert!(root.path().join("codex-thread-id").is_file()); + started.session.kill().unwrap(); + drop(started); + let mut resumed = CodexAdapter.start(&spec).expect("native thread resume"); + resumed.session.kill().unwrap(); +} diff --git a/crates/gravityd/tests/codex_runtime.rs b/crates/gravityd/tests/codex_runtime.rs new file mode 100644 index 0000000..0685228 --- /dev/null +++ b/crates/gravityd/tests/codex_runtime.rs @@ -0,0 +1,121 @@ +use gravityd::runtime::codex::{CodexAdapter, CodexSpec}; +use gravityd::runtime::{BotSpec, RuntimeAdapter, SessionEvent, StartedSession}; +use serde_json::Value; +use std::path::Path; +use std::time::Duration; + +fn spec(root: &Path) -> BotSpec { + let workspace = root.join("workspace"); + std::fs::create_dir_all(&workspace).unwrap(); + std::fs::write(root.join("system.md"), "Shared system instructions").unwrap(); + BotSpec { + bot_id: "test".into(), + bot_name: "Test".into(), + workspace, + claude_bin: "unused".into(), + claude_args: Vec::new(), + codex: Some(CodexSpec { + bin: std::env::var("NODE_BINARY").unwrap_or_else(|_| "node".into()), + args: vec![Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/codex-server.mjs") + .display() + .to_string()], + port: 49777, + artifacts: Some(root.join("artifacts")), + }), + env: vec![ + ("GRAVITY_TOKEN".into(), "test-token".into()), + ( + "GRAVITY_TEST_LOG".into(), + root.join("rpc.jsonl").display().to_string(), + ), + ], + cols: 80, + rows: 24, + } +} + +fn log(root: &Path) -> Vec { + std::fs::read_to_string(root.join("rpc.jsonl")) + .unwrap() + .lines() + .map(|line| serde_json::from_str(line).unwrap()) + .collect() +} + +async fn hook(started: &mut StartedSession, name: &str) { + tokio::time::timeout(Duration::from_secs(5), async { + loop { + match started.events.recv().await.unwrap() { + SessionEvent::Lifecycle { event, .. } if event == name => break, + SessionEvent::Exited { code } => panic!("unexpected exit {code:?}"), + _ => {} + } + } + }) + .await + .unwrap(); +} + +#[tokio::test] +async fn structured_delivery_does_not_consume_terminal_draft_and_resumes_history() { + let root = tempfile::tempdir().unwrap(); + let spec = spec(root.path()); + let mut started = CodexAdapter.start(&spec).unwrap(); + hook(&mut started, "SessionStart").await; + started.session.send_input("draft 🪟".as_bytes()).unwrap(); + started.session.deliver("bus envelope").unwrap().unwrap(); + hook(&mut started, "Stop").await; + started.session.send_input(b"\r").unwrap(); + hook(&mut started, "Stop").await; + let turns: Vec<_> = log(root.path()) + .into_iter() + .filter(|v| v["method"] == "turn/start") + .collect(); + assert_eq!(turns[0]["params"]["input"][0]["text"], "bus envelope"); + assert_eq!(turns[1]["params"]["input"][0]["text"], "draft 🪟"); + let observed = std::fs::read_to_string(root.path().join("codex-observations.jsonl")).unwrap(); + assert!(observed.contains("Hello 🪟")); + started.session.kill().unwrap(); + drop(started); + let mut resumed = CodexAdapter.start(&spec).unwrap(); + hook(&mut resumed, "SessionStart").await; + assert!(log(root.path()) + .iter() + .any(|v| v["method"] == "thread/resume" && v["params"]["threadId"] == "thread-fixture")); + resumed.session.kill().unwrap(); +} + +#[tokio::test] +async fn approvals_are_explicit_and_interruption_never_reports_success() { + let root = tempfile::tempdir().unwrap(); + let mut started = CodexAdapter.start(&spec(root.path())).unwrap(); + started.session.deliver("approval").unwrap().unwrap(); + hook(&mut started, "Notification").await; + assert!(!log(root.path()) + .iter() + .any(|v| v["id"] == "approval-1" && v.get("result").is_some())); + started.session.send_input(b"/deny 1\r").unwrap(); + hook(&mut started, "Stop").await; + assert!(log(root.path()) + .iter() + .any(|v| v["id"] == "approval-1" && v["result"]["decision"] == "decline")); + started.session.deliver("hold").unwrap().unwrap(); + started.session.deliver("steered message").unwrap().unwrap(); + assert!(log(root.path()) + .iter() + .any(|v| v["method"] == "turn/steer" && v["params"]["expectedTurnId"] == "turn-fixture")); + started.session.send_input(b"\x03").unwrap(); + hook(&mut started, "TurnInterrupted").await; + started.session.deliver("crash").unwrap().unwrap(); + tokio::time::timeout(Duration::from_secs(5), async { + loop { + if let SessionEvent::Exited { code } = started.events.recv().await.unwrap() { + assert_eq!(code, Some(7)); + break; + } + } + }) + .await + .unwrap(); +} diff --git a/crates/gravityd/tests/fixtures/codex-server.mjs b/crates/gravityd/tests/fixtures/codex-server.mjs new file mode 100644 index 0000000..728ec4b --- /dev/null +++ b/crates/gravityd/tests/fixtures/codex-server.mjs @@ -0,0 +1,41 @@ +import { appendFileSync } from "node:fs"; +import { createInterface } from "node:readline"; + +const log = process.env.GRAVITY_TEST_LOG; +const send = (value) => { process.stdout.write(`${JSON.stringify(value)}\n`); }; +const reply = (id, result) => { send({ id, result }); }; +const notify = (method, params) => { send({ method, params }); }; +let active = false; +for await (const line of createInterface({ input: process.stdin })) { + const request = JSON.parse(line); + appendFileSync(log, `${line}\n`); + const { id, method, params } = request; + if (method === "initialize") { reply(id, { userAgent: "fixture" }); } + else if (method === "initialized") { /* notification */ } + else if (method === "thread/start" || method === "thread/resume") { + reply(id, { thread: { id: "thread-fixture" } }); + } else if (method === "turn/start") { + const text = params.input[0].text; + active = true; + notify("turn/started", { turn: { id: "turn-fixture", status: "inProgress" } }); + reply(id, { turn: { id: "turn-fixture", status: "inProgress" } }); + if (text === "approval") { + send({ id: "approval-1", method: "item/commandExecution/requestApproval", params: { command: "echo hello", threadId: "thread-fixture", turnId: "turn-fixture" } }); + } else if (text === "crash") { process.exit(7); } + else if (text !== "hold") { + notify("item/agentMessage/delta", { delta: "Hello 🪟" }); + notify("item/completed", { item: { type: "agentMessage", text: "Hello 🪟" } }); + active = false; + notify("turn/completed", { turn: { id: "turn-fixture", status: "completed" } }); + } + } else if (method === "turn/steer") { reply(id, { turnId: "turn-fixture" }); } + else if (method === "turn/interrupt") { + reply(id, {}); + active = false; + notify("turn/completed", { turn: { id: "turn-fixture", status: "interrupted" } }); + } else if (id === "approval-1" && request.result) { + if (!active) { throw new Error("approval after completed turn"); } + active = false; + notify("turn/completed", { turn: { id: "turn-fixture", status: "completed" } }); + } else { throw new Error(`Unexpected request ${line}`); } +} diff --git a/crates/gravityd/tests/windows_inbox.rs b/crates/gravityd/tests/windows_inbox.rs index d3b1957..1247bb4 100644 --- a/crates/gravityd/tests/windows_inbox.rs +++ b/crates/gravityd/tests/windows_inbox.rs @@ -7,6 +7,7 @@ use gravityd::runtime::{BotSpec, RuntimeAdapter, SessionEvent}; #[tokio::test] async fn windows_inbox_delivers_authenticated_unicode_messages() { let spec = BotSpec { + codex: None, bot_id: "windows-inbox".into(), bot_name: "Inbox".into(), workspace: std::env::temp_dir(), diff --git a/crates/gravityd/tests/windows_pty.rs b/crates/gravityd/tests/windows_pty.rs index eb75a17..b702575 100644 --- a/crates/gravityd/tests/windows_pty.rs +++ b/crates/gravityd/tests/windows_pty.rs @@ -7,6 +7,7 @@ use gravityd::runtime::{BotSpec, RuntimeAdapter, SessionEvent}; #[tokio::test] async fn conpty_streams_a_native_process_and_reports_exit() { let spec = BotSpec { + codex: None, bot_id: "windows-pty".into(), bot_name: "PTY".into(), workspace: std::env::temp_dir(), @@ -30,6 +31,7 @@ async fn conpty_streams_a_native_process_and_reports_exit() { assert_eq!(code, Some(0)); return bytes; } + SessionEvent::Lifecycle { .. } => {} } } panic!("process must report exit"); diff --git a/docs/bot-runtimes.md b/docs/bot-runtimes.md new file mode 100644 index 0000000..e446036 --- /dev/null +++ b/docs/bot-runtimes.md @@ -0,0 +1,55 @@ +# Bot runtimes + +Gravity can run Claude Code and Codex CLI bots in the same project. Install and +sign in to the selected CLI on the daemon's computer. In **Bot info → Bot runtime**, +choose the CLI and click **Change runtime**. This interrupts the active turn and +restarts the bot. Its workspace, instructions, saved facts, bus messages and routines +remain available. The two providers keep separate conversations; switching back +resumes that provider's saved conversation. + +![Runtime picker verified in the native Windows app](images/bot-runtime-picker.png) + +Existing bots and new bots use Claude Code by default. For a Codex-only installation, +set this in `~/.gravity/gravityd.toml` and restart the daemon: + +```toml +default_bot_runtime = "codex_cli" +codex_bin = "codex" +codex_args = [] +``` + +Use an absolute `codex_bin` or `claude_bin` path when the daemon service cannot find +the CLI on its PATH. Bot-authored children inherit their creator's runtime. Control +clients may also pass `runtime` when creating a bot. The `runtime = "double"` daemon +setting continues to select the deterministic test adapter for every bot. + +## Codex terminal + +Codex runs through [Codex App Server](https://learn.chatgpt.com/docs/app-server) +over stdio. Tested against Codex CLI 0.159.2. The terminal displays streamed text +and command output; type a prompt and press Enter. `/interrupt` or Ctrl+C cancels +the active turn. Bus messages arrive through structured turn requests and never +consume a partially typed terminal prompt. Terminal resize is cosmetic: this +adapter does not render the Codex full-screen CLI interface. + +Approval requests appear with numbered prompts. `/approve 1` grants the requested +command, file change or permissions; `/deny 1` declines it. Permissions are granted +only for the current turn. Requests are never automatically approved. Questions use +`/answer 1 {"answers":{"question-id":{"answers":["your answer"]}}}`. MCP elicitation +uses `/answer 1 {"action":"accept","content":{"field":"value"}}`; `/deny 1` declines it. +Unsupported server requests fail explicitly. + +Each bot receives its own Gravity bus token through an environment variable; the +App Server config enables the local Gravity MCP endpoint. Codex uses workspace-write +sandboxing with the project's shared artifacts directory included. It reads the +shared system instructions, `CLAUDE.md` and `FACTS.md`. Thread IDs and activity +observations are saved beside the workspace for restarts and routine correlation. +Interrupted or failed turns do not report successful routine completion. + +## Upgrade + +The additive SQLite migration sets existing bots to `claude_code`. Update the +daemon and client together to expose the picker; the client hides it for older +daemons. Keep a database backup before upgrading, as older daemons do not understand +Codex bots. CLI authentication and a live model response require the user's account; +the test suite uses a local App Server fixture without network or model charges. diff --git a/docs/images/bot-runtime-picker.png b/docs/images/bot-runtime-picker.png new file mode 100644 index 0000000000000000000000000000000000000000..718cdb7e17e2716e14626c7331cd6d91fef41812 GIT binary patch literal 7842 zcmai(bx<7Ly6%DC9)bi58ro>RMI(_S^kD>sh}QrLC!ihfRTvgoK2rqAah2goG^eJO*K+JYVr? z|I{NP5hAI`1N6MJ4zh9F^`=_~yVaSTlW-YPBco7JFzJag>FD!1QrLh61`XOB_T27< z#x#|l9x(kMk+YZ8)qdPYew|*Xj*VefH7!oo#h(D>EH$!7y>h@!0M8Hu0UsS>u*7lD zR>x)BJc}k<{i}DOO1#Ud~B!HDbVQTzu0E z4%r?%B{Z86K*;qfA4Q)q)F~ze6|i%uoQDm>OOzo&rCf!)ZM*9)iK<6z$um^>IUnEj zI@G~pJ}sO2gfKyhc2dqb&jD-c1sC0hDaQPt##KFPGk2zEuXOs28n=(sM@&0_EH4)a z_l##Nmlf@nM`?Ync7F>;M`VBKrnWni&8spy6&kT`D?DzxfJ+QKo| z&COGcZKU2g^&5H6=I-=H_hr{GKz)nWz*KJVN}tDog0Z3b)b=!jpu)TyEly0K{-~#f zmp!ezg~Cs(Q5Yruev=pxUN2K6c6k-BfTo$C&AFdk%J|T0cuF_`+2_J9Fx=em;bkjgalX&NV z6F24vo4D(CmEE=_$Mq$%6m@#bH!7UWt;?DkfU*yHBa?@(MruatpI0()fJpB$NRSBnqS?#A_81KvtG@h0G)Lxe)#@5&=BRHfMr^4$gjXKd2>Rm2ePC=gwLS zTYsihE^)Qj<9H=K@K*LIj4sZUpm8cJ#Q2y>xKGf3avb)S?})^g%^btNG9-K}cH`61 zyz)^e4Mz*7?XS}QYAJGpSc)Xy#f{vfs*=k*Un4EAw&Eh^-8(Pt40!<*x2EZSzY+p3 z`er}?-@tgf@kEv~3H+80?Lw~+e#P*$9%Xrof@Q(aW&PJ}NS#kZ5IdGEK|lXF+)uB- z4{-tUduj$1p2S@!ob4X=UqOpYi;qZ+z{U4{KW0OqS14k`Ud>An2Q5Hxn$kza_P!8^ zd#BmwBt&T>Ud}@yGtp~UGYM5cXnoS>5ulxq(5?$GGXXGhS+H7&Rl3{-%#n!LI(erD$eh` zysoSPQ!m@M_v{xIj8vYT0SR~_t1B1U`#o)`q;Ac`CigFLi?%wB%$$Nw>OSvR2m?LJsYC=LR`{=xq?!V`}usfYTj44Tgmf)#* zLkmDEsQ^O9vD=G%t!%NV)y)rZma$>-gzO;0r<@$VIhiq^c>ETOiSRGs58y~rC$q+b zDZQg3QXQ!XngXx$X#XShT{v%4Gj#JDbF@)_KpYfDb2hm{@5}WBQZvvhfbB58OjZgX z^S1?bCmcy;C^rC6^9O^!-9IJ;>C}}MxF1n%h^f=z28^+uO7LANKd}^>nSz3;i+kD9dB}kK(QE_Af<1|Qx|K#QG z)8es;A;I7d2S!cvu6BzxgEX`CY7%gly)lNa_G{*|%Z0*F)LWGsVmQCS)>Jc_iG6lVA(`i;+VLA|`TubP{Ks9!e^mEdT6*WxmoM?r@oxcy zZErEPP|FIRr7sJVC}hTj5r;AZ1&F8}YKoR$;z!)$Hq*Kb6A_tC)VX@tu}j9tn|92l zq%AQsF*U*DD6xPprhqGQ>Q|tYC5(4Q#%@0gJ7M4h`XHwLLTp*#o~zr^XK7z4b}2oF zgnvo*|1Ii&vZ#NXs3aK@)O80_q6xhIPR{>vorIcF+-9fN_>Ad|NNui|l95q2jl@ka zBdDUNC9RDpYo~57*JZX1np1UiDGA<&UCF4ZsL08^2piXxzMs(O4Zo`t|B4@D2>JE6 zOHNMuoMNV zsj2C~V*^9n%Tc(5&KXidpsbVuZnWzbxkXdIzus6`Z2ID0ztXia5EqjW=XK$kZ+rW& zx3cD5``By;J9JmH(%J|c@S3U8wbf3aZF8WYr1&o9dh#x1t=bTr=(u!!a$8onO+yoa zE1G=OHq#1ACr{s>dEoZV`7!f@Zhn5~sdp^%sPjsyXd6ds`J;DV9G^l7@ zUg5LKu-w^pvp_RGl{bTd=sY}wZy0=z50VxYO~?D~435BE!=?uq`WUL_>T&shHYE31 z#6PVxy4&oL!$^dFdwijDSCsclLxXH3@UE((!?=~v*Yuj`q1x504r2HHVVu;I_2=cs$$U2Mp{8e9Dh5SRv1A z^B!1m2>CHl(wpZlO$?33W7ZV>m;CB*(aw|KHe&v2d>#S1IPk7rE7yODb8N*{P*4cE z?zOkId-z~0)5Q6h0b9;J>5b&~bMe&hXq;DM!8Xr1=J*g9O~+^MyNhC;f3`X{?y1kIcg88=ZPG_Ui5WOhZa|c zI^uDyP4&N2_I9-fER#}=QTgL~{(4v25ja45W@GWz8+IIc#k4r$uhD8I^8Lv4dg$Pi zcnBnxFtvgKhuivh=`wRynHDzNuq2D(Ep&uL5V%5mMRCckq$7ld8zQ*`R$Ta?IqOzb z`!z@aQYdalkW;@a$?*OW&k?S3~i9Xv(bCUm%<{G)WT zJj<0<)#LcU?}YxC~tw;rNiqftLE=^gEg7la7D7t zx7|uIw^Vf&p}O1Ez38$1SFb-NezSfBK7pK9_onw!}$U?Tv{6+V7*Me$!0BLeN0D}rsd_qXq<+=j@PWa&GCF*B&TuBi; z4}3Qmrwk1undXT&C?GI&vVd7hy38JXUr;EVr!F@orSEaz3cusk7`>C+`o=5Swx43z z=f$+Hr-W3^DN9~w4|SsNdAE83geDPv3D6FqFAGCA2%Dsfs5QB;I~*Su+waUiN+PfE z))nE`C)^KjnQXfCM5jRfi; zZ8iNO9$2vBm33gldYVE=MwH@P)ScsW}70slGck<%gnZ zA+E0b(7cO=K69BLNOZg6tXO+DX+>RjA9!~FYxml%{unG(GP?n=#e2mABqU@GBM_X| zSVkWfQ@t>Bc8X9YM3o+@A^c~PYZvezA^N{lXcD9<)1^~D9(f{6li0}}D|2BOyXO3y zva)Kz0}VXhs#ff&`uVkDe-;dDdzN3eMP`SoJ?Un`?RJY!RgPctB7g~pSm*B2b~oK{ zsgIA|Kf3qBJs_}xc5C{X2&nzT0#Kz?6;UJn5HxRYHlaq~9zA-D44(ci^QxVQ; zgk7(ROHn(BiO*Wx+^!a~9Zr@POi&F3V70_J zz;|A3_*_UNOvlKyB6QO@8z|{?@BKIZ!emozRo7HMH7g~w`B`5~mwTxPUoFMi7KZ7y zq^JjZq(`f9`qr&BJ|H0`)woxuQvT{q*I9G}Xv3%KBFa!`Ee8A@Vkf!iA}INt^x8OQ z_FXk@PP}pjZOO@``dLRtM_UA2H{#3M)+!Hwj#_DhOxqvvF(&n`Kat&+-Cv(bzO~=T zeqX2XJYi*Q_xq5ayoc4iih>Schtk2i_KhNlw zNkykhsV?blSF=kRqsOqMCQAn=C~12hQ7toFs4jH}Npo3dFUF0tiHaJQRVSi*iL_*0 zD}$tLZ9e|)yU!zn;5iT2zPF|W%W(J&5gvhBkXqP=-Z!>*$Gk~W!@LHv-cP5nmpb%m z6M2^hRyM-4l^fRYF-6!k%@QW`t*r-Q>)zk6>J9FLfd-^yj#kU^bFb`_P(q9|xk-gu zFg}jbI^v2N?=O5*?|xPF+F-@e?rhg0;>%qVD+!|}S?o_}ZliuNZn^Mz)-{e+MRi$x zbo9X6$ONrYcJ^XzViy=;3BVw1840-z9Tz#_iW$*UY*n_|RF$il^HsbF53t@W^Zng5 zj$Ci$y&6}GM9Z>NZp0fA7g~C94elizyjCbK9)lSg#cra#J#8MWV>|=4v;@J{3bE%< zHl(76`9`9*DvLm(jJT#+*d>d}9o%}O__+j#0kd{6#zDuiPv{x1jqMSJDB_z4S z=?unGd+k9!cZ@-XHuI^3GcMtiv;3+CXj5Pah33y9na5hf>paur*&6c{er%zBbFDK& zlgf(fRp5@qSiviS$!9NgP5xQm-#_qH3Eze&Qnaw(54Yv(sHNK?@0>9CsTjvpJ#3Gj z&T1{V-mi@MzcC7yEs&@vl`R2MPq_eIC$CoDUNuO09j$)g(0m&Z{R0jfLt!!h`|XjK zFw~ZA1>O{<2{F4DJ8xWHs4ri}`_IoGQ1al!%7M58%ASdWwm`sE%ExI+qCCeCFnG<# z+p@5{z8nKGEP6p6rsid6R!qRemO+tONzg=ijVMj^dv_luIB%G`7tJNz;(TP>LLRtR zMibzAj`?$U8W)dhfrGFY4nR)Kd%PDt73xHw&+MMXqS0rqP;Akqtd5#%ydzuE|4MOY zqa)M4CSO&>c_8NPpdFWw=qbHVbo+(PhW111P<~lTqkHsO8L`qn1(AYL;aNU>!xUN6 zKJ}0>i|5~kQ2JBrxteoRe(<+F>6U223-q8~Vk`;5MVH@h+xx?hbG!NeFC)zJg4miL%npp?q(}`rkbHc67=8JE&jPo-}P1aoQYh^4iuNu-8%9 zZ?NsbE4&e_Ns=^TwxG79 zFZD6ja^@u*ZjTJ*<&|?*0fuE>78Yg}n)}`6h7^&Z5&}Z*SKO~D!@U4!2(99Fb~rA`q%2#W?PVsaS9rz59?!zls;s9 z=d20>CQ`j9Zf(DNIO=u7=$wDC)9~9^$a^i#mt99hq+l{=B#-laz?aukFotC5o;A%KZlGL( zXpmYSJTdeGJo0|(flNwJht!S(5vquxr&FJv$`~^BjBopuH=|>+(WT~6XDDuaQ9-%% z;`*I+Sd;(?m|1e*E|tn6}9CX~LjpVip2yMypxelR$?XATWN< z&QBB8(UJe99kV8|0Fupw832`FZ-R8>A5OpAY+8rt{h`PB3tMTGoupi6t94|L-B}~N z&s(r06y9|<`~jcR@R`qY6&0<#1#2hs+K+L7oknqukYgKtOgZ(ih9R9!h-VgS4-iF^jax8DTWPuacY zYn+^!u8J$uGI3=ne@_FJLTi@COw+cL!aI|>m9;<)^N4T0e&m#-hWuw7SaUOE$Wa!LzT=RbC0I^IUnn4etqpIXA zWFUawp_!p9&hLXffSZ(!{$@}a8z-sN)w3= zvwL_XCPuR_Sq6pJ!lDYHSe)I?ew>b@vKEEnpCg|PX`)b<)z5dG0cC`N79Y|;sQFVO zJhI7M4Wp5W36Gdd2DBH=@$z7AQD#N&7Wk!cC%*~^4IfDH`|;!e4f z%M)hC#%i7~!g7T!;?vU7Mn+WkC=hrWYG-Ifq2n(X=-(j@uv#|?3kqsslauxB76kKg zeSLk4Ov}Efn`I#p_;gZF^=kn{p-x(u!D4$qjKgtdd}==HVt^izdSeyKSXXO~Z(OEoc3b7NzEcYg`X zVm_3>O*I$(*9*PYSzi7bjictfp^B&f<>tf-G%+-UGvGuNYUN6~EFFq_{sx25@i)&0 z6N1v8j&cd8B+`;HfJd{5L_Hx~86DIfI$tNZx&uP{zEpjQWIM&z9*n;s-Tub%tSO$p zs)N`5yd8(DI0YHC(i+|PZA%+D0m_4%1Jm6xEv0Gu*ptS+lAI|tV9E^d9L zZ*I93`PYN4N)L9Vto0}U&h=4B*u6SFe6tv!(y2Gnba^@OQ_&5DO5APWyk=#fUzhn8 z>w;L7NH3?D8Hmr(5%&`w0F$f-eJ_1v1Oka|Xz1!``6&gCKJ4hgB8_s*Ed%m!ad9p9 zy->+t!Z4#G+_ym?($EFrizZI z0<`~<(-2HQ;Kw*nIv(4Y6yqi>(T$oAvzP$^6}g$U&2_PU3)0Tj*j3}DE%e*e+!hpL z(3(RrE$JGVliMi#Lx4xkgYspxp)vb3rF*O>Z4!OY@U@z;An4ZO2G&!}0HmiFAN^yn z!x4WOFGSRa%UeT8N>#-<#wclTFtH|BL+eheq;}SS#J|7T`^^%5bsAQ?>a*H&7IH*= zI9U@;g8Eq;R}DyrF9GhNK0MUGyQsfZ_fBlI3*}mULPkP*dWyTSYI;6tM*nA9`2s0N X&b8yqq>sz<9V8V6P5EkB3*dhNOB-9i literal 0 HcmV?d00001 diff --git a/docs/protocol.md b/docs/protocol.md index 5ee97fa..a1f254c 100644 --- a/docs/protocol.md +++ b/docs/protocol.md @@ -54,7 +54,8 @@ Codes: `auth_failed`, `unsupported_version`, `not_found`, `invalid_request`, | `delete_project` | `project_id` | `ok` — archives the project and every bot in it; see Semantics | | `list_bots` | `project_id?` | `bots` | | `list_bot_activity` | `project_id?` | `bot_activity` — one preview line per bot; see Semantics | -| `create_bot` | `project_id, name?, description?, instructions?, avatar?` | `bot` (starts running immediately) | +| `create_bot` | `project_id, name?, description?, instructions?, avatar?, runtime?` | `bot` (starts running immediately) | +| `set_bot_runtime` | `bot_id, runtime` | `bot` (requires `control`; restarts when changed) | | `update_bot` | `bot_id, name?, description?, instructions?, avatar?` | `bot` | | `delete_bot` | `bot_id, reason?` | `ok` — archives the bot; see Semantics | | `list_bot_revisions` | `bot_id, limit?` | `bot_revisions` | @@ -108,6 +109,13 @@ When `create_bot` omits `name` — or sends it as `null` — the daemon allocate available placeholder in the project: `New Bot`, `New Bot 2`, and so on, retrying if another client claims the same one first. Explicitly named bot creation is unchanged. +Daemons advertising `bot_runtime` include `runtime: "claude_code" | "codex_cli"` +in bot objects. Older daemons omit it and use Claude Code. `create_bot` defaults +to the configured `default_bot_runtime`; bot-authored children inherit their +creator's runtime. `set_bot_runtime` preserves the workspace and provider histories, +interrupts the current turn, and restarts through the supervisor. Setting the +current value is a no-op. Existing bots migrate to `claude_code`. + `trigger` is `{ "kind": "cron", "expr": "0 0 9 * * MON", "tz": "Europe/Warsaw" }`, `{ "kind": "interval", "seconds": 3600 }`, or `{ "kind": "signal", "name": "deploy.finished", "from_bot_id": "..." }`. diff --git a/ops/gravityd.example.toml b/ops/gravityd.example.toml index daaf95b..6daaf91 100644 --- a/ops/gravityd.example.toml +++ b/ops/gravityd.example.toml @@ -21,11 +21,14 @@ port = 49777 # a collision. negotiate_port = true -# "pty" spawns the `claude` CLI in a pseudo-terminal per bot. +# "pty" selects each bot's saved CLI (Claude Code PTY or Codex App Server). # "double" is a deterministic echo runtime for testing. runtime = "pty" claude_bin = "claude" claude_args = [] +codex_bin = "codex" +codex_args = [] +default_bot_runtime = "claude_code" # or "codex_cli" for newly created bots # Tokens a bot conversation may reach before Claude Code auto-compacts it # (exported as CLAUDE_CODE_AUTO_COMPACT_WINDOW). Every turn re-sends the whole From f0986956177b631519adedbd3a31d0441532763f Mon Sep 17 00:00:00 2001 From: Manuel Rinaldi Date: Thu, 1 Oct 2026 11:14:49 +0300 Subject: [PATCH 05/55] fix(windows): stop native sessions and normalize trust paths --- crates/gravityd/src/paths/trust.rs | 47 ++++++- crates/gravityd/src/runtime/pty.rs | 12 +- crates/gravityd/src/runtime/pty/windows.rs | 50 ++++++++ crates/gravityd/tests/claude_native.rs | 73 +++++++++++ .../gravityd/tests/fixtures/codex-server.mjs | 2 +- crates/gravityd/tests/fixtures/live-pty.mjs | 6 + crates/gravityd/tests/windows_pty.rs | 119 ++++++++++++++++++ 7 files changed, 305 insertions(+), 4 deletions(-) create mode 100644 crates/gravityd/src/runtime/pty/windows.rs create mode 100644 crates/gravityd/tests/claude_native.rs create mode 100644 crates/gravityd/tests/fixtures/live-pty.mjs diff --git a/crates/gravityd/src/paths/trust.rs b/crates/gravityd/src/paths/trust.rs index d6b85a6..d4b0111 100644 --- a/crates/gravityd/src/paths/trust.rs +++ b/crates/gravityd/src/paths/trust.rs @@ -92,9 +92,12 @@ fn project_key(path: &Path) -> anyhow::Result { #[cfg(windows)] { if let Some(rest) = path.strip_prefix(r"\\?\UNC\") { - return Ok(format!(r"\\{rest}")); + return Ok(format!("//{}", rest.replace('\\', "/"))); } - Ok(path.strip_prefix(r"\\?\").unwrap_or(path).to_string()) + Ok(path + .strip_prefix(r"\\?\") + .unwrap_or(path) + .replace('\\', "/")) } #[cfg(not(windows))] Ok(path.to_string()) @@ -165,6 +168,46 @@ mod tests { #[cfg(unix)] use std::os::unix::fs::PermissionsExt; + #[cfg(windows)] + #[test] + fn windows_keys_match_claudes_forward_slashes() { + assert_eq!( + project_key(Path::new(r"C:\bots\one")).unwrap(), + "C:/bots/one" + ); + assert_eq!( + project_key(Path::new(r"\\?\C:\bots\one")).unwrap(), + "C:/bots/one" + ); + assert_eq!( + project_key(Path::new(r"\\?\UNC\server\share\one")).unwrap(), + "//server/share/one" + ); + } + + #[cfg(windows)] + #[test] + fn trust_updates_the_entry_claude_reads_without_forking_project_state() { + let tmp = tempfile::tempdir().unwrap(); + let workspace = tmp.path().join("workspace"); + fs::create_dir(&workspace).unwrap(); + let node_key = workspace.to_str().unwrap().replace('\\', "/"); + let config_path = tmp.path().join(".claude.json"); + fs::write( + &config_path, + serde_json::to_vec(&serde_json::json!({"projects": { + node_key.clone(): {"hasTrustDialogAccepted": false, "otherSetting": "preserved"} + }})) + .unwrap(), + ) + .unwrap(); + trust_workspace(tmp.path(), &workspace).unwrap(); + let config = read_claude_config(&config_path).unwrap(); + assert!(is_trusted(&config, &node_key)); + assert_eq!(config["projects"][&node_key]["otherSetting"], "preserved"); + assert_eq!(config["projects"].as_object().unwrap().len(), 1); + } + /// A temp home with a workspace and a `.claude.json` holding `config`. fn trust_fixture(config: &str) -> (tempfile::TempDir, PathBuf, PathBuf) { let tmp = tempfile::tempdir().expect("tmp"); diff --git a/crates/gravityd/src/runtime/pty.rs b/crates/gravityd/src/runtime/pty.rs index 7b5020f..49d9e7a 100644 --- a/crates/gravityd/src/runtime/pty.rs +++ b/crates/gravityd/src/runtime/pty.rs @@ -9,7 +9,11 @@ use std::process::Command as StdCommand; use std::time::{Duration, Instant}; use anyhow::Context; -use portable_pty::{native_pty_system, ChildKiller, CommandBuilder, MasterPty, PtySize}; +#[cfg(unix)] +use portable_pty::ChildKiller; +use portable_pty::{native_pty_system, CommandBuilder, MasterPty, PtySize}; +#[cfg(windows)] +mod windows; use tokio::sync::mpsc; use super::{BotSpec, Capabilities, RuntimeAdapter, RuntimeSession, SessionEvent, StartedSession}; @@ -73,7 +77,10 @@ impl RuntimeAdapter for PtyAdapter { let master_fd = pair.master.as_raw_fd(); let (tx, rx) = mpsc::unbounded_channel(); + #[cfg(unix)] let killer = child.clone_killer(); + #[cfg(windows)] + let killer = windows::ProcessKiller::new(child.as_ref())?; // ConPTY keeps its read pipe open until the master is dropped. Waiting // for reader EOF first would hide exits forever, preventing restarts. @@ -227,7 +234,10 @@ fn utf8_prefix_len(bytes: &[u8]) -> usize { struct PtySession { master: Box, writer: Box, + #[cfg(unix)] killer: Box, + #[cfg(windows)] + killer: windows::ProcessKiller, } impl RuntimeSession for PtySession { diff --git a/crates/gravityd/src/runtime/pty/windows.rs b/crates/gravityd/src/runtime/pty/windows.rs new file mode 100644 index 0000000..cde73bc --- /dev/null +++ b/crates/gravityd/src/runtime/pty/windows.rs @@ -0,0 +1,50 @@ +//! Own the child handle so termination does not depend on portable-pty 0.8's +//! inverted WinChildKiller return-value check. +use std::io; +use std::os::windows::io::{AsRawHandle, BorrowedHandle, OwnedHandle, RawHandle}; + +#[link(name = "kernel32")] +extern "system" { + fn GetExitCodeProcess(process: RawHandle, code: *mut u32) -> i32; + fn TerminateProcess(process: RawHandle, code: u32) -> i32; +} + +pub(super) struct ProcessKiller(OwnedHandle); + +impl ProcessKiller { + pub fn new(child: &dyn portable_pty::Child) -> anyhow::Result { + let raw = child + .as_raw_handle() + .ok_or_else(|| anyhow::anyhow!("PTY child has no process handle"))?; + // SAFETY: the child owns this valid process handle for this entire call. + // Duplicate it before the child moves to its independent waiter thread. + let borrowed = unsafe { BorrowedHandle::borrow_raw(raw) }; + Ok(Self(borrowed.try_clone_to_owned()?)) + } + + fn exited(&self) -> io::Result { + let mut code = 0; + // SAFETY: our owned process handle is live; code is a valid output pointer. + if unsafe { GetExitCodeProcess(self.0.as_raw_handle(), &mut code) } == 0 { + return Err(io::Error::last_os_error()); + } + Ok(code != 259) // STILL_ACTIVE + } + + pub fn kill(&mut self) -> io::Result<()> { + if self.exited()? { + return Ok(()); + } + // SAFETY: the duplicated handle refers only to this session's child. + if unsafe { TerminateProcess(self.0.as_raw_handle(), 1) } != 0 { + return Ok(()); + } + let error = io::Error::last_os_error(); + // The child can finish between the status check and termination. + if self.exited()? { + Ok(()) + } else { + Err(error) + } + } +} diff --git a/crates/gravityd/tests/claude_native.rs b/crates/gravityd/tests/claude_native.rs new file mode 100644 index 0000000..04c09b8 --- /dev/null +++ b/crates/gravityd/tests/claude_native.rs @@ -0,0 +1,73 @@ +#![cfg(windows)] +//! Opt-in native CLI startup check, isolated from account data and model traffic. +use gravityd::runtime::pty::PtyAdapter; +use gravityd::runtime::{BotSpec, RuntimeAdapter, SessionEvent}; +use serde_json::json; +use std::time::Duration; + +#[tokio::test] +#[ignore = "requires CLAUDE_BINARY pointing to a locally installed CLI"] +async fn native_claude_reaches_its_prompt_without_asking_for_workspace_trust() { + let root = tempfile::tempdir().unwrap(); + let workspace = root.path().join("workspace"); + let config = root.path().join("config"); + std::fs::create_dir(&workspace).unwrap(); + std::fs::create_dir(&config).unwrap(); + let fake_key = "sk-ant-gravity-smoke-local-only"; + std::fs::write(config.join(".claude.json"), serde_json::to_vec(&json!({ + "hasCompletedOnboarding": true, "lastOnboardingVersion": "2.1.286", "numStartups": 1, "theme": "dark", + "customApiKeyResponses": {"approved": [fake_key.chars().skip(fake_key.len() - 20).collect::()], "rejected": []} + })).unwrap()).unwrap(); + gravityd::paths::trust_workspace(&config, &workspace).unwrap(); + let spec = BotSpec { + codex: None, + bot_id: "native-trust".into(), + bot_name: "Trust check".into(), + workspace, + claude_bin: std::env::var("CLAUDE_BINARY").expect("CLAUDE_BINARY"), + claude_args: vec![ + "--strict-mcp-config".into(), + "--mcp-config".into(), + "{\"mcpServers\":{}}".into(), + ], + env: vec![ + ("CLAUDE_CONFIG_DIR".into(), config.display().to_string()), + ("ANTHROPIC_API_KEY".into(), fake_key.into()), + ("ANTHROPIC_BASE_URL".into(), "http://127.0.0.1:1".into()), + ( + "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC".into(), + "1".into(), + ), + ("CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN".into(), "1".into()), + ], + cols: 120, + rows: 40, + }; + let mut started = PtyAdapter.start(&spec).expect("native Claude process"); + let mut output = String::new(); + let ready = tokio::time::timeout(Duration::from_secs(20), async { + while let Some(event) = started.events.recv().await { + match event { + SessionEvent::Output(bytes) => { + output.push_str(&String::from_utf8_lossy(&bytes)); + if output.contains("shift+tab") { + return true; + } + } + SessionEvent::Exited { .. } => return false, + SessionEvent::Lifecycle { .. } => {} + } + } + false + }) + .await; + started.session.kill().unwrap(); + assert!( + matches!(ready, Ok(true)), + "Claude did not reach its prompt: {output}" + ); + assert!( + !output.to_ascii_lowercase().contains("trust"), + "unexpected trust request: {output}" + ); +} diff --git a/crates/gravityd/tests/fixtures/codex-server.mjs b/crates/gravityd/tests/fixtures/codex-server.mjs index 728ec4b..e84b193 100644 --- a/crates/gravityd/tests/fixtures/codex-server.mjs +++ b/crates/gravityd/tests/fixtures/codex-server.mjs @@ -1,7 +1,7 @@ import { appendFileSync } from "node:fs"; import { createInterface } from "node:readline"; -const log = process.env.GRAVITY_TEST_LOG; +const log = process.env.GRAVITY_TEST_LOG ?? "../codex-fixture-rpc.jsonl"; const send = (value) => { process.stdout.write(`${JSON.stringify(value)}\n`); }; const reply = (id, result) => { send({ id, result }); }; const notify = (method, params) => { send({ method, params }); }; diff --git a/crates/gravityd/tests/fixtures/live-pty.mjs b/crates/gravityd/tests/fixtures/live-pty.mjs new file mode 100644 index 0000000..53a3241 --- /dev/null +++ b/crates/gravityd/tests/fixtures/live-pty.mjs @@ -0,0 +1,6 @@ +import { appendFileSync } from "node:fs"; + +appendFileSync("../pty-launches", "started\n"); +process.stdout.write("ready-for-runtime-change\n"); +process.stdin.resume(); +setInterval(() => {}, 1000); diff --git a/crates/gravityd/tests/windows_pty.rs b/crates/gravityd/tests/windows_pty.rs index b702575..8cd28d2 100644 --- a/crates/gravityd/tests/windows_pty.rs +++ b/crates/gravityd/tests/windows_pty.rs @@ -1,8 +1,10 @@ #![cfg(windows)] +mod common; use std::time::Duration; use gravityd::runtime::pty::PtyAdapter; use gravityd::runtime::{BotSpec, RuntimeAdapter, SessionEvent}; +use serde_json::json; #[tokio::test] async fn conpty_streams_a_native_process_and_reports_exit() { @@ -40,4 +42,121 @@ async fn conpty_streams_a_native_process_and_reports_exit() { .await .expect("PTY exit"); assert!(String::from_utf8_lossy(&bytes).contains("gravity-conpty-ok")); + started + .session + .kill() + .expect("stopping an exited process is harmless"); +} + +#[tokio::test] +async fn conpty_kills_a_live_process_without_reporting_a_stale_handle_error() { + let spec = BotSpec { + codex: None, + bot_id: "windows-kill".into(), + bot_name: "Kill".into(), + workspace: std::env::temp_dir(), + claude_bin: "powershell.exe".into(), + claude_args: vec![ + "-NoProfile".into(), + "-Command".into(), + "Write-Output 'waiting-for-switch'; Start-Sleep -Seconds 120".into(), + ], + env: Vec::new(), + cols: 80, + rows: 24, + }; + let mut started = PtyAdapter.start(&spec).expect("native ConPTY"); + tokio::time::timeout(Duration::from_secs(15), async { + loop { + if let Some(SessionEvent::Output(bytes)) = started.events.recv().await { + if String::from_utf8_lossy(&bytes).contains("waiting-for-switch") { + break; + } + } + } + }) + .await + .expect("live child output"); + started + .session + .kill() + .expect("terminate a live process successfully"); + tokio::time::timeout(Duration::from_secs(5), async { + loop { + if let Some(SessionEvent::Exited { .. }) = started.events.recv().await { + break; + } + } + }) + .await + .expect("killed child reports exit"); + started.session.kill().expect("repeated stop is harmless"); +} + +#[tokio::test] +async fn control_plane_switches_a_live_conpty_to_codex_and_back() { + let fixtures = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures"); + let daemon = common::spawn_daemon_with(|cfg| { + cfg.runtime = gravityd::config::RuntimeKind::Pty; + cfg.supervision_interval_ms = 50; + cfg.user_home = cfg.home.join("cli-home"); + std::fs::create_dir_all(&cfg.user_home).unwrap(); + cfg.claude_bin = "node".into(); + cfg.claude_args = vec![fixtures.join("live-pty.mjs").display().to_string()]; + cfg.codex_bin = "node".into(); + cfg.codex_args = vec![fixtures.join("codex-server.mjs").display().to_string()]; + }) + .await; + let mut client = common::WsClient::connect(&daemon).await; + let project = client + .request(json!({"type":"create_project", "name":"Switch"})) + .await; + let bot = common::create_bot( + &mut client, + project["project"]["id"].as_str().unwrap(), + "Native", + ) + .await; + let id = bot["id"].as_str().unwrap(); + let root = std::path::Path::new(bot["workspace_path"].as_str().unwrap()) + .parent() + .unwrap(); + let wait = async { + while !root.join("pty-launches").is_file() + || daemon.app.supervisor.state(id).0 != bus::BotState::Ready + { + tokio::time::sleep(Duration::from_millis(20)).await; + } + }; + tokio::time::timeout(Duration::from_secs(10), wait) + .await + .unwrap(); + let reply = client + .request(json!({"type":"set_bot_runtime", "bot_id":id, "runtime":"codex_cli"})) + .await; + assert_eq!(reply["bot"]["runtime"], "codex_cli", "{reply}"); + tokio::time::timeout(Duration::from_secs(10), async { + while !root.join("codex-thread-id").is_file() + || daemon.app.supervisor.state(id).0 != bus::BotState::Ready + { + tokio::time::sleep(Duration::from_millis(20)).await; + } + }) + .await + .expect("Codex starts after terminating the native PTY"); + let reply = client + .request(json!({"type":"set_bot_runtime", "bot_id":id, "runtime":"claude_code"})) + .await; + assert_eq!(reply["bot"]["runtime"], "claude_code", "{reply}"); + tokio::time::timeout(Duration::from_secs(10), async { + while !std::fs::read_to_string(root.join("pty-launches")) + .is_ok_and(|text| text.lines().count() >= 2) + || daemon.app.supervisor.state(id).0 != bus::BotState::Ready + { + tokio::time::sleep(Duration::from_millis(20)).await; + } + }) + .await + .expect("native PTY starts again"); + daemon.app.supervisor.stop_bot(id).unwrap(); } From 9704a69c7c59beca4ca654bcf94b27aada8bd126 Mon Sep 17 00:00:00 2001 From: Manuel Rinaldi Date: Thu, 1 Oct 2026 11:59:49 +0300 Subject: [PATCH 06/55] fix(runtime): discover Windows Codex and surface startup failures --- .../src/components/bot/BotRuntimePicker.tsx | 4 +- crates/gravityd/src/runtime/codex.rs | 17 +- crates/gravityd/src/runtime/executable.rs | 149 ++++++++++++++++++ crates/gravityd/src/runtime/mixed.rs | 18 ++- crates/gravityd/src/runtime/mod.rs | 5 + crates/gravityd/src/supervisor.rs | 2 + crates/gravityd/src/supervisor/lifecycle.rs | 4 +- crates/gravityd/src/supervisor/restart.rs | 25 +++ crates/gravityd/src/ws/runtime.rs | 4 + crates/gravityd/tests/windows_pty.rs | 116 +++++++++++++- docs/windows.md | 9 ++ 11 files changed, 328 insertions(+), 25 deletions(-) create mode 100644 crates/gravityd/src/runtime/executable.rs diff --git a/apps/desktop/src/components/bot/BotRuntimePicker.tsx b/apps/desktop/src/components/bot/BotRuntimePicker.tsx index a8b874c..07b53ab 100644 --- a/apps/desktop/src/components/bot/BotRuntimePicker.tsx +++ b/apps/desktop/src/components/bot/BotRuntimePicker.tsx @@ -37,8 +37,8 @@ export default function BotRuntimePicker({ onBotUpdated(reply.bot); onToast( "info", - "Runtime updated", - `${reply.bot.name} will restart with ${runtime === "codex_cli" ? "Codex CLI" : "Claude Code"}.`, + "Runtime saved", + `${reply.bot.name} is restarting with ${runtime === "codex_cli" ? "Codex CLI" : "Claude Code"}. Startup progress appears in the terminal.`, ); } catch (error) { onToast("error", "Runtime update failed", errText(error)); diff --git a/crates/gravityd/src/runtime/codex.rs b/crates/gravityd/src/runtime/codex.rs index 2440e8a..6fc1a9d 100644 --- a/crates/gravityd/src/runtime/codex.rs +++ b/crates/gravityd/src/runtime/codex.rs @@ -1,7 +1,7 @@ //! Codex CLI App Server over stdio JSONL. Bus input is a separate RPC stream. use std::io::{BufRead, BufReader}; use std::path::{Path, PathBuf}; -use std::process::{Child, Command, Stdio}; +use std::process::{Child, Stdio}; use std::sync::{mpsc, Arc, Mutex}; use std::time::Duration; @@ -48,16 +48,14 @@ impl RuntimeAdapter for CodexAdapter { } } fn probe(&self) -> anyhow::Result { - let out = Command::new("codex").arg("--version").output()?; - anyhow::ensure!(out.status.success(), "codex --version failed"); - Ok(String::from_utf8_lossy(&out.stdout).trim().to_string()) + super::executable::version("codex") } fn start(&self, spec: &BotSpec) -> anyhow::Result { let codex = spec .codex .as_ref() .context("missing Codex runtime settings")?; - let mut command = Command::new(&codex.bin); + let mut command = super::executable::command(&codex.bin); command .args(&codex.args) .arg("app-server") @@ -68,12 +66,9 @@ impl RuntimeAdapter for CodexAdapter { .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()); - #[cfg(windows)] - { - use std::os::windows::process::CommandExt; - command.creation_flags(0x08000000); - } - let mut child = command.spawn().context("spawn Codex CLI App Server")?; + let mut child = command + .spawn() + .with_context(|| format!("spawn Codex CLI App Server using {}", codex.bin))?; let stdin = child.stdin.take().context("Codex stdin")?; let stdout = child.stdout.take().context("Codex stdout")?; let stderr = child.stderr.take().context("Codex stderr")?; diff --git a/crates/gravityd/src/runtime/executable.rs b/crates/gravityd/src/runtime/executable.rs new file mode 100644 index 0000000..53bfdb4 --- /dev/null +++ b/crates/gravityd/src/runtime/executable.rs @@ -0,0 +1,149 @@ +//! Resolve user-installed CLIs without relying on a desktop-only PATH. +use std::process::Command; + +pub(super) fn command(bin: &str) -> Command { + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + let path = if matches!(bin, "codex" | "codex.exe") { + windows_codex( + std::env::var_os("PATH").as_deref(), + std::env::var_os("APPDATA").as_deref(), + std::env::var_os("LOCALAPPDATA").as_deref(), + ) + } else { + None + }; + let mut command = Command::new(path.unwrap_or_else(|| bin.into())); + command.creation_flags(0x08000000); + command + } + #[cfg(not(windows))] + Command::new(bin) +} + +pub(super) fn version(bin: &str) -> anyhow::Result { + use anyhow::Context; + let out = command(bin) + .arg("--version") + .output() + .with_context(|| format!("could not run {bin} --version"))?; + anyhow::ensure!(out.status.success(), "{bin} --version failed"); + Ok(String::from_utf8_lossy(&out.stdout).trim().to_string()) +} + +#[cfg(windows)] +fn windows_codex( + path: Option<&std::ffi::OsStr>, + appdata: Option<&std::ffi::OsStr>, + localappdata: Option<&std::ffi::OsStr>, +) -> Option { + use std::path::PathBuf; + // A CLI installed on PATH takes precedence over the desktop's bundled CLI. + for directory in path.into_iter().flat_map(std::env::split_paths) { + for name in ["codex.exe", "codex.cmd"] { + let candidate = directory.join(name); + if candidate.is_file() { + return Some(candidate); + } + } + } + if let Some(appdata) = appdata { + let npm = PathBuf::from(appdata).join("npm/codex.cmd"); + if npm.is_file() { + return Some(npm); + } + } + let local = PathBuf::from(localappdata?); + for suffix in ["Programs/OpenAI/Codex/bin/codex.exe", "pnpm/codex.cmd"] { + let installed = local.join(suffix); + if installed.is_file() { + return Some(installed); + } + } + // Codex desktop uses versioned directories that are absent from the + // Task Scheduler environment. Re-resolve on each start after app updates. + let root = local.join("OpenAI/Codex/bin"); + std::fs::read_dir(root) + .ok()? + .filter_map(Result::ok) + .filter_map(|entry| { + let candidate = entry.path().join("codex.exe"); + let metadata = candidate.metadata().ok()?; + metadata + .is_file() + .then_some((metadata.modified().ok()?, candidate)) + }) + .max_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(&b.1))) + .map(|(_, candidate)| candidate) +} + +#[cfg(all(test, windows))] +mod tests { + use super::*; + use std::path::Path; + + fn file(path: &Path) { + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + std::fs::write(path, "fixture").unwrap(); + } + + #[test] + fn finds_desktop_codex_with_a_service_path_and_prefers_the_installed_cli() { + let root = tempfile::tempdir().unwrap(); + let appdata = root.path().join("roaming"); + let local = root.path().join("local"); + let desktop = local.join("OpenAI/Codex/bin/build-a/codex.exe"); + file(&desktop); + assert_eq!( + windows_codex(None, Some(appdata.as_os_str()), Some(local.as_os_str())), + Some(desktop) + ); + let cli = local.join("Programs/OpenAI/Codex/bin/codex.exe"); + file(&cli); + assert_eq!( + windows_codex(None, Some(appdata.as_os_str()), Some(local.as_os_str())), + Some(cli) + ); + let npm = appdata.join("npm/codex.cmd"); + file(&npm); + assert_eq!( + windows_codex(None, Some(appdata.as_os_str()), Some(local.as_os_str())), + Some(npm) + ); + let installed = root.path().join("cli/codex.exe"); + file(&installed); + let path = std::env::join_paths([installed.parent().unwrap()]).unwrap(); + assert_eq!( + windows_codex( + Some(&path), + Some(appdata.as_os_str()), + Some(local.as_os_str()) + ), + Some(installed) + ); + assert_eq!(windows_codex(None, None, None), None); + } + + #[test] + fn explicit_executable_paths_are_preserved() { + assert_eq!( + command("C:/custom/codex.exe").get_program(), + "C:/custom/codex.exe" + ); + } + + #[test] + #[ignore = "requires installed Codex CLI; no model request"] + fn installed_codex_runs_without_the_desktop_path() { + let bin = windows_codex(None, None, std::env::var_os("LOCALAPPDATA").as_deref()) + .expect("installed Codex CLI"); + let out = command(&bin.to_string_lossy()) + .arg("--version") + .env("PATH", "C:/Windows/System32;C:/Windows") + .output() + .unwrap(); + assert!(out.status.success(), "{out:?}"); + assert!(String::from_utf8_lossy(&out.stdout).contains("codex")); + } +} diff --git a/crates/gravityd/src/runtime/mixed.rs b/crates/gravityd/src/runtime/mixed.rs index 6671edf..09ebf37 100644 --- a/crates/gravityd/src/runtime/mixed.rs +++ b/crates/gravityd/src/runtime/mixed.rs @@ -18,6 +18,15 @@ impl MixedAdapter { } impl RuntimeAdapter for MixedAdapter { + fn check_available(&self, runtime: bus::BotRuntime) -> anyhow::Result<()> { + let (bin, setting) = match runtime { + bus::BotRuntime::ClaudeCode => (&self.claude_bin, "claude_bin"), + bus::BotRuntime::CodexCli => (&self.codex_bin, "codex_bin"), + }; + super::executable::version(bin).map(|_| ()).map_err(|error| { + anyhow::anyhow!("{error:#}. Install the CLI or set {setting} to its executable path in gravityd.toml.") + }) + } fn capabilities(&self) -> Capabilities { Capabilities { kind: "cli", @@ -36,14 +45,7 @@ impl RuntimeAdapter for MixedAdapter { fn probe(&self) -> anyhow::Result { let versions: Vec = [&self.claude_bin, &self.codex_bin] .into_iter() - .filter_map(|bin| { - std::process::Command::new(bin) - .arg("--version") - .output() - .ok() - .filter(|out| out.status.success()) - .map(|out| String::from_utf8_lossy(&out.stdout).trim().to_string()) - }) + .filter_map(|bin| super::executable::version(bin).ok()) .collect(); anyhow::ensure!(!versions.is_empty(), "neither configured CLI is available"); Ok(versions.join("; ")) diff --git a/crates/gravityd/src/runtime/mod.rs b/crates/gravityd/src/runtime/mod.rs index a7ddac1..3e2f170 100644 --- a/crates/gravityd/src/runtime/mod.rs +++ b/crates/gravityd/src/runtime/mod.rs @@ -6,6 +6,7 @@ pub mod codex; pub mod double; +mod executable; #[cfg(windows)] mod inbox; pub mod mixed; @@ -80,6 +81,10 @@ pub trait RuntimeSession: Send { } pub trait RuntimeAdapter: Send + Sync { + /// Check the configured CLI before changing a bot's saved provider. + fn check_available(&self, _runtime: bus::BotRuntime) -> anyhow::Result<()> { + Ok(()) + } fn capabilities(&self) -> Capabilities; /// Start a session. Output and exit events arrive on the returned channel. fn start(&self, spec: &BotSpec) -> anyhow::Result; diff --git a/crates/gravityd/src/supervisor.rs b/crates/gravityd/src/supervisor.rs index 3c003fe..da541e4 100644 --- a/crates/gravityd/src/supervisor.rs +++ b/crates/gravityd/src/supervisor.rs @@ -101,6 +101,7 @@ struct BotHandle { reason: String, session: Option>>>, term: Arc, + terminal_runtime: Option, /// Inbox socket for channel delivery; from the adapter (double) or the /// SessionStart hook (pty). msg_socket: Option, @@ -138,6 +139,7 @@ impl BotHandle { reason: String::new(), session: None, term: Arc::new(TermBuffer::new(scrollback)), + terminal_runtime: None, msg_socket: None, size: (0, 0), repaint_restore: None, diff --git a/crates/gravityd/src/supervisor/lifecycle.rs b/crates/gravityd/src/supervisor/lifecycle.rs index 3fb3a8c..f13d79c 100644 --- a/crates/gravityd/src/supervisor/lifecycle.rs +++ b/crates/gravityd/src/supervisor/lifecycle.rs @@ -156,6 +156,7 @@ impl Supervisor { let handle = bots .entry(bot_id.to_string()) .or_insert_with(|| BotHandle::new(self.inner.cfg.scrollback_bytes)); + handle.prepare_terminal(bot.runtime); handle.session = Some(session.clone()); handle.msg_socket = started.msg_socket; handle.size = (spec.cols, spec.rows); @@ -234,10 +235,11 @@ impl Supervisor { continue; } if let Err(e) = self.start_bot(&bot.id) { - tracing::warn!(bot_id = %bot.id, error = %e, "autostart failed"); + tracing::warn!(bot_id = %bot.id, error = %format!("{e:#}"), "autostart failed"); // Back off like a crash so an unresolvable runtime does not // respawn on every tick. self.note_failed_start(&bot.id); + self.report_start_failure(&bot.id, &e); } } } diff --git a/crates/gravityd/src/supervisor/restart.rs b/crates/gravityd/src/supervisor/restart.rs index 247ff58..1a1a4d2 100644 --- a/crates/gravityd/src/supervisor/restart.rs +++ b/crates/gravityd/src/supervisor/restart.rs @@ -1,6 +1,31 @@ use super::*; +impl BotHandle { + pub(super) fn prepare_terminal(&mut self, runtime: bus::BotRuntime) { + if self + .terminal_runtime + .is_some_and(|previous| previous != runtime) + { + self.term.push(b"\x1bc".to_vec()); + } + self.terminal_runtime = Some(runtime); + } +} + impl Supervisor { + pub(super) fn report_start_failure(&self, bot_id: &str, error: &anyhow::Error) { + let (state, _) = self.state(bot_id); + let reason = format!("Runtime failed to start: {error:#}"); + self.set_state(bot_id, BotState::Crashed, &reason); + if state != BotState::Crashed { + if let Some(term) = self.term(bot_id) { + term.push(format!("\r\n[Gravity] {reason}\r\n").into_bytes()); + } + self.inner + .events + .push(Push::notice("error", "Bot could not start", reason)); + } + } pub fn restart_bot(&self, bot_id: &str) -> anyhow::Result<()> { let previous = self.state(bot_id); let session = { diff --git a/crates/gravityd/src/ws/runtime.rs b/crates/gravityd/src/ws/runtime.rs index 188e395..4527c33 100644 --- a/crates/gravityd/src/ws/runtime.rs +++ b/crates/gravityd/src/ws/runtime.rs @@ -29,6 +29,10 @@ impl Conn { .get_live_bot(bot_id)? .ok_or_else(|| anyhow::anyhow!("bot not found"))?; if bot.runtime != runtime { + if let Err(error) = self.app.supervisor.adapter().check_available(runtime) { + self.reply_err(req_id, "runtime_unavailable", &format!("{error:#}")); + return Ok(()); + } self.app.db.set_bot_runtime(bot_id, runtime)?; if let Err(error) = self.app.supervisor.restart_bot(bot_id) { self.app.db.set_bot_runtime(bot_id, bot.runtime)?; diff --git a/crates/gravityd/tests/windows_pty.rs b/crates/gravityd/tests/windows_pty.rs index 8cd28d2..40821f4 100644 --- a/crates/gravityd/tests/windows_pty.rs +++ b/crates/gravityd/tests/windows_pty.rs @@ -95,18 +95,37 @@ async fn conpty_kills_a_live_process_without_reporting_a_stale_handle_error() { #[tokio::test] async fn control_plane_switches_a_live_conpty_to_codex_and_back() { + let fixture = + std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/codex-server.mjs"); + check_provider_switch("node".into(), vec![fixture.display().to_string()]).await; +} + +#[tokio::test] +#[ignore = "requires installed Codex CLI, NODE_BINARY and an isolated CODEX_HOME; no model turn"] +async fn native_codex_switch_works_with_the_windows_service_path() { + assert!( + std::env::var_os("CODEX_HOME").is_some(), + "isolated CODEX_HOME required" + ); + check_provider_switch("codex".into(), Vec::new()).await; +} + +async fn check_provider_switch(codex_bin: String, codex_args: Vec) { let fixtures = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures"); let daemon = common::spawn_daemon_with(|cfg| { cfg.runtime = gravityd::config::RuntimeKind::Pty; cfg.supervision_interval_ms = 50; cfg.user_home = cfg.home.join("cli-home"); std::fs::create_dir_all(&cfg.user_home).unwrap(); - cfg.claude_bin = "node".into(); + cfg.claude_bin = std::env::var("NODE_BINARY").unwrap_or_else(|_| "node".into()); cfg.claude_args = vec![fixtures.join("live-pty.mjs").display().to_string()]; - cfg.codex_bin = "node".into(); - cfg.codex_args = vec![fixtures.join("codex-server.mjs").display().to_string()]; + cfg.codex_bin = codex_bin; + cfg.codex_args = codex_args; + cfg.delivery.poll_interval_ms = 3_600_000; }) .await; + // Let the empty initial delivery poll finish; this test sends no model turn. + tokio::time::sleep(Duration::from_millis(100)).await; let mut client = common::WsClient::connect(&daemon).await; let project = client .request(json!({"type":"create_project", "name":"Switch"})) @@ -144,6 +163,18 @@ async fn control_plane_switches_a_live_conpty_to_codex_and_back() { }) .await .expect("Codex starts after terminating the native PTY"); + let terminal = daemon.app.supervisor.term(id).unwrap(); + let replay: Vec = terminal + .replay_after(0) + .frames + .into_iter() + .flat_map(|frame| frame.data) + .collect(); + let output = String::from_utf8_lossy(&replay); + let reset = output + .rfind("\x1bc") + .expect("clear the previous provider's screen"); + assert!(output[reset..].contains("Codex CLI ready"), "{output}"); let reply = client .request(json!({"type":"set_bot_runtime", "bot_id":id, "runtime":"claude_code"})) .await; @@ -160,3 +191,82 @@ async fn control_plane_switches_a_live_conpty_to_codex_and_back() { .expect("native PTY starts again"); daemon.app.supervisor.stop_bot(id).unwrap(); } + +#[tokio::test] +async fn missing_codex_is_rejected_before_stopping_a_live_claude_session() { + let fixtures = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures"); + let daemon = common::spawn_daemon_with(|cfg| { + cfg.runtime = gravityd::config::RuntimeKind::Pty; + cfg.user_home = cfg.home.join("cli-home"); + std::fs::create_dir_all(&cfg.user_home).unwrap(); + cfg.claude_bin = "node".into(); + cfg.claude_args = vec![fixtures.join("live-pty.mjs").display().to_string()]; + cfg.codex_bin = cfg.home.join("missing-codex.exe").display().to_string(); + }) + .await; + let mut client = common::WsClient::connect(&daemon).await; + let project = client + .request(json!({"type":"create_project", "name":"Unavailable"})) + .await; + let bot = common::create_bot( + &mut client, + project["project"]["id"].as_str().unwrap(), + "Running", + ) + .await; + let id = bot["id"].as_str().unwrap(); + let reply = client + .request(json!({"type":"set_bot_runtime", "bot_id":id, "runtime":"codex_cli"})) + .await; + assert_eq!(reply["code"], "runtime_unavailable", "{reply}"); + assert!(reply["message"].as_str().unwrap().contains("codex_bin")); + assert_eq!( + daemon.app.db.get_bot(id).unwrap().unwrap().runtime, + bus::BotRuntime::ClaudeCode + ); + assert_eq!(daemon.app.supervisor.active_total(), 1); + assert_ne!(daemon.app.supervisor.state(id).0, bus::BotState::Stopping); + daemon.app.supervisor.stop_bot(id).unwrap(); +} + +#[tokio::test] +async fn codex_startup_failure_is_reported_in_state_and_terminal() { + let daemon = common::spawn_daemon_with(|cfg| { + cfg.runtime = gravityd::config::RuntimeKind::Pty; + cfg.supervision_interval_ms = 50; + cfg.codex_bin = "node".into(); + cfg.codex_args = vec!["-e".into(), "process.exit(7)".into(), "--".into()]; + }) + .await; + let mut client = common::WsClient::connect(&daemon).await; + let project = client + .request(json!({"type":"create_project", "name":"Failed"})) + .await; + let bot = client.request(json!({"type":"create_bot", "project_id":project["project"]["id"], "name":"Failed", "runtime":"codex_cli"})).await; + let id = bot["bot"]["id"].as_str().unwrap(); + tokio::time::timeout(Duration::from_secs(5), async { + while daemon.app.supervisor.state(id).0 != bus::BotState::Crashed { + tokio::time::sleep(Duration::from_millis(20)).await; + } + }) + .await + .expect("startup failure is visible"); + assert!(daemon + .app + .supervisor + .state(id) + .1 + .contains("failed to start")); + let output: Vec = daemon + .app + .supervisor + .term(id) + .unwrap() + .replay_after(0) + .frames + .into_iter() + .flat_map(|frame| frame.data) + .collect(); + assert!(String::from_utf8_lossy(&output).contains("[Gravity] Runtime failed to start")); + assert_eq!(daemon.app.supervisor.active_total(), 0); +} diff --git a/docs/windows.md b/docs/windows.md index 24ab8aa..e048e9f 100644 --- a/docs/windows.md +++ b/docs/windows.md @@ -48,6 +48,15 @@ pipe reported by its SessionStart hook. PowerShell hooks serialize the pipe path as JSON and report lifecycle events to the local daemon. A native Windows daemon cannot use a Claude inbox inside WSL; install the native CLI for native bots. +For Codex bots, install and sign in to Codex CLI on the daemon host, then choose +**Bot info → Bot runtime → Codex CLI**. Gravity searches PATH, the usual npm/pnpm +and native Windows CLI locations, then the Codex desktop bundle. An explicit +`codex_bin` path in `gravityd.toml` overrides discovery. The Windows task can +have an older PATH than a newly opened terminal, so custom installation paths +should be configured explicitly. Missing CLIs are rejected before the current +bot session is stopped; later startup failures appear in the bot state and +terminal. Switching providers clears the previous provider's terminal screen. + For development without model credentials, set `runtime = "double"` in a private `gravityd.toml` and run `cargo run -p gravityd -- --config `. The double uses an authenticated named pipe and exercises the same message-delivery path. From f0855967460c18c17b5e4046c05c0611368e3cc5 Mon Sep 17 00:00:00 2001 From: Manuel Rinaldi Date: Thu, 1 Oct 2026 13:36:03 +0300 Subject: [PATCH 07/55] feat(runtime): embed the native Codex terminal --- README.md | 2 +- crates/gravityd/Cargo.toml | 2 +- crates/gravityd/src/runtime/codex.rs | 180 +++++++++++------- .../gravityd/src/runtime/codex/approvals.rs | 7 + crates/gravityd/src/runtime/codex/input.rs | 63 ++++++ crates/gravityd/src/runtime/codex/native.rs | 140 ++++++++++++++ .../gravityd/src/runtime/codex/transport.rs | 118 ++++++++++++ crates/gravityd/src/runtime/codex/worker.rs | 118 +++++------- crates/gravityd/src/runtime/mixed.rs | 4 +- crates/gravityd/src/runtime/pty.rs | 7 +- crates/gravityd/tests/codex_native.rs | 144 +++++++++++++- crates/gravityd/tests/codex_runtime.rs | 66 ++++++- .../tests/fixtures/codex-responses.mjs | 37 ++++ .../gravityd/tests/fixtures/codex-server.mjs | 22 ++- .../tests/fixtures/codex-websocket.mjs | 123 ++++++++++++ crates/gravityd/tests/windows_pty.rs | 14 +- docs/bot-runtimes.md | 45 +++-- third-party/inventory.json | 2 +- 18 files changed, 927 insertions(+), 167 deletions(-) create mode 100644 crates/gravityd/src/runtime/codex/input.rs create mode 100644 crates/gravityd/src/runtime/codex/native.rs create mode 100644 crates/gravityd/src/runtime/codex/transport.rs create mode 100644 crates/gravityd/tests/fixtures/codex-responses.mjs create mode 100644 crates/gravityd/tests/fixtures/codex-websocket.mjs diff --git a/README.md b/README.md index 4bce851..55084a6 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ runtime works without it. See [public-build configuration](docs/public-builds.md for telemetry, releases, and secret scanning. Choose each bot's CLI in **Bot info → Bot runtime**. See -[bot runtimes](docs/bot-runtimes.md) for setup, configuration and Codex terminal commands. +[bot runtimes](docs/bot-runtimes.md) for setup, configuration and the native Codex terminal. ## Layout diff --git a/crates/gravityd/Cargo.toml b/crates/gravityd/Cargo.toml index d43cc4b..4ffca78 100644 --- a/crates/gravityd/Cargo.toml +++ b/crates/gravityd/Cargo.toml @@ -28,8 +28,8 @@ sha2 = { workspace = true } hex = { workspace = true } futures = { workspace = true } bytes = { workspace = true } +tokio-tungstenite = "0.24" [dev-dependencies] tempfile = "3" -tokio-tungstenite = "0.24" reqwest = { version = "0.12", features = ["json"] } diff --git a/crates/gravityd/src/runtime/codex.rs b/crates/gravityd/src/runtime/codex.rs index 6fc1a9d..8ac0aba 100644 --- a/crates/gravityd/src/runtime/codex.rs +++ b/crates/gravityd/src/runtime/codex.rs @@ -10,9 +10,14 @@ use anyhow::Context; use serde_json::Value; mod approvals; +mod input; +mod native; mod observations; +mod transport; mod worker; +pub use native::NativeCodexAdapter; + #[derive(Debug, Clone)] pub struct CodexSpec { pub bin: String, @@ -51,30 +56,65 @@ impl RuntimeAdapter for CodexAdapter { super::executable::version("codex") } fn start(&self, spec: &BotSpec) -> anyhow::Result { - let codex = spec - .codex - .as_ref() - .context("missing Codex runtime settings")?; - let mut command = super::executable::command(&codex.bin); - command - .args(&codex.args) - .arg("app-server") - .args(["--listen", "stdio://"]) - .current_dir(&spec.workspace) - .envs(spec.env.iter().cloned()) - .env_remove("CODEX_THREAD_ID") - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()); - let mut child = command - .spawn() - .with_context(|| format!("spawn Codex CLI App Server using {}", codex.bin))?; - let stdin = child.stdin.take().context("Codex stdin")?; - let stdout = child.stdout.take().context("Codex stdout")?; - let stderr = child.stderr.take().context("Codex stderr")?; - let child = Arc::new(Mutex::new(child)); - let (tx, rx) = mpsc::channel(); - let (events_tx, events_rx) = tokio::sync::mpsc::unbounded_channel(); + start_server(spec, None) + } +} + +fn start_server( + spec: &BotSpec, + remote: Option<&transport::Remote>, +) -> anyhow::Result { + let codex = spec + .codex + .as_ref() + .context("missing Codex runtime settings")?; + let mut command = super::executable::command(&codex.bin); + command + .args(&codex.args) + .arg("app-server") + .current_dir(&spec.workspace) + .envs(spec.env.iter().cloned()) + .env_remove("CODEX_THREAD_ID") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + if let Some(remote) = remote { + command.args([ + "--listen", + &remote.address, + "--ws-auth", + "capability-token", + "--ws-token-sha256", + &remote.verifier(), + ]); + } else { + command.args(["--listen", "stdio://"]); + } + let mut child = command + .spawn() + .with_context(|| format!("spawn Codex CLI App Server using {}", codex.bin))?; + let stdin = child.stdin.take().context("Codex stdin")?; + let stdout = child.stdout.take().context("Codex stdout")?; + let stderr = child.stderr.take().context("Codex stderr")?; + let child = Arc::new(Mutex::new(child)); + let (tx, rx) = mpsc::channel(); + let (events_tx, events_rx) = tokio::sync::mpsc::unbounded_channel(); + let native = remote.is_some(); + let input = if let Some(remote) = remote { + drop(stdin); + std::thread::spawn( + move || { + for _ in BufReader::new(stdout).lines().map_while(Result::ok) {} + }, + ); + match transport::connect(remote, tx.clone(), child.clone()) { + Ok(input) => input, + Err(error) => { + let _ = child.lock().unwrap_or_else(|e| e.into_inner()).kill(); + return Err(error); + } + } + } else { let reader_tx = tx.clone(); std::thread::spawn(move || { let mut reader = BufReader::new(stdout); @@ -94,57 +134,61 @@ impl RuntimeAdapter for CodexAdapter { } let _ = reader_tx.send(Wire::Closed); }); - let err_tx = events_tx.clone(); - let stderr_tail = Arc::new(Mutex::new(String::new())); - let captured = stderr_tail.clone(); - std::thread::spawn(move || { - for line in BufReader::new(stderr).lines().map_while(Result::ok) { - let mut tail = captured.lock().unwrap_or_else(|e| e.into_inner()); - if tail.len() + line.len() > 32_768 { - tail.clear(); - } - tail.push_str(&line); - tail.push('\n'); - drop(tail); - if err_tx + transport::RpcWriter::Stdio(std::io::BufWriter::new(stdin)) + }; + let err_tx = events_tx.clone(); + let stderr_tail = Arc::new(Mutex::new(String::new())); + let captured = stderr_tail.clone(); + std::thread::spawn(move || { + for line in BufReader::new(stderr).lines().map_while(Result::ok) { + let mut tail = captured.lock().unwrap_or_else(|e| e.into_inner()); + if tail.len() + line.len() > 32_768 { + tail.clear(); + } + tail.push_str(&line); + tail.push('\n'); + drop(tail); + if !native + && err_tx .send(SessionEvent::Output( format!("[codex] {line}\r\n").into_bytes(), )) .is_err() - { - break; - } + { + break; } - }); - let (ready_tx, ready_rx) = mpsc::sync_channel(1); - let spec = spec.clone(); - let worker_child = child.clone(); - std::thread::spawn(move || worker::run(spec, stdin, rx, events_tx, ready_tx, worker_child)); - let ready = ready_rx - .recv_timeout(Duration::from_secs(30)) - .context("Codex initialization timed out"); - match ready { - Ok(Ok(())) => Ok(StartedSession { - session: Box::new(CodexSession { tx, child }), - events: events_rx, - msg_socket: None, - }), - outcome => { - let _ = child.lock().unwrap_or_else(|e| e.into_inner()).kill(); - let detail = stderr_tail - .lock() - .unwrap_or_else(|e| e.into_inner()) - .clone(); - match outcome { - Ok(Err(error)) | Err(error) => { - if detail.trim().is_empty() { - Err(error) - } else { - Err(error).context(detail.trim().to_string()) - } + } + }); + let (ready_tx, ready_rx) = mpsc::sync_channel(1); + let spec = spec.clone(); + let worker_child = child.clone(); + std::thread::spawn(move || { + worker::run(spec, input, native, rx, events_tx, ready_tx, worker_child) + }); + let ready = ready_rx + .recv_timeout(Duration::from_secs(30)) + .context("Codex initialization timed out"); + match ready { + Ok(Ok(())) => Ok(StartedSession { + session: Box::new(CodexSession { tx, child }), + events: events_rx, + msg_socket: None, + }), + outcome => { + let _ = child.lock().unwrap_or_else(|e| e.into_inner()).kill(); + let detail = stderr_tail + .lock() + .unwrap_or_else(|e| e.into_inner()) + .clone(); + match outcome { + Ok(Err(error)) | Err(error) => { + if detail.trim().is_empty() { + Err(error) + } else { + Err(error).context(detail.trim().to_string()) } - Ok(Ok(())) => unreachable!(), } + Ok(Ok(())) => unreachable!(), } } } diff --git a/crates/gravityd/src/runtime/codex/approvals.rs b/crates/gravityd/src/runtime/codex/approvals.rs index 14957ef..dd3b9d7 100644 --- a/crates/gravityd/src/runtime/codex/approvals.rs +++ b/crates/gravityd/src/runtime/codex/approvals.rs @@ -9,6 +9,13 @@ pub(super) struct Prompt { impl Worker { pub fn prompt(&mut self, id: Value, method: &str, params: Value) -> anyhow::Result<()> { + if self.native { + self.hook( + "Notification", + Some("Codex needs your response in the terminal".into()), + ); + return Ok(()); + } if !matches!( method, "item/commandExecution/requestApproval" diff --git a/crates/gravityd/src/runtime/codex/input.rs b/crates/gravityd/src/runtime/codex/input.rs new file mode 100644 index 0000000..0cab78d --- /dev/null +++ b/crates/gravityd/src/runtime/codex/input.rs @@ -0,0 +1,63 @@ +//! Legacy headless input used by protocol tests. Native bots use the real TUI. +use super::worker::Worker; +use crate::runtime::SessionEvent; +use anyhow::Context; +use serde_json::json; + +impl Worker { + pub fn keystrokes(&mut self, bytes: &[u8]) -> anyhow::Result<()> { + for &byte in bytes { + if self.escaping { + if byte != b'[' && (0x40..=0x7e).contains(&byte) { + self.escaping = false; + } + continue; + } + match byte { + 27 => self.escaping = true, + 3 => { + self.line.clear(); + self.interrupt()?; + } + 8 | 127 => { + if let Some(last) = self.line.pop() { + if last & 0xc0 == 0x80 { + while self.line.pop().is_some_and(|b| b & 0xc0 == 0x80) {} + } + self.output("\u{8} \u{8}"); + } + } + 13 | 10 if !self.line.is_empty() => { + let text = String::from_utf8(std::mem::take(&mut self.line)) + .context("prompt is not UTF-8")?; + self.output("\n"); + if text == "/interrupt" { + self.interrupt()?; + } else if !self.answer(&text)? { + self.deliver(&text)?; + } + } + 13 | 10 => {} + b if b >= 32 => self.line.push(b), + _ => {} + } + } + // Echo whole UTF-8 inputs rather than one byte per output frame. + if !bytes + .iter() + .any(|b| matches!(b, 3 | 8 | 10 | 13 | 27 | 127)) + { + let _ = self.events.send(SessionEvent::Output(bytes.to_vec())); + } + Ok(()) + } + fn interrupt(&mut self) -> anyhow::Result<()> { + if let Some(turn) = &self.turn { + self.rpc( + "turn/interrupt", + json!({ "threadId": self.thread, "turnId": turn }), + )?; + } + Ok(()) + } +} diff --git a/crates/gravityd/src/runtime/codex/native.rs b/crates/gravityd/src/runtime/codex/native.rs new file mode 100644 index 0000000..4c5d418 --- /dev/null +++ b/crates/gravityd/src/runtime/codex/native.rs @@ -0,0 +1,140 @@ +//! The official Codex terminal UI and Gravity share one private App Server. +use super::{start_server, transport::Remote, CodexAdapter}; +use crate::runtime::{ + pty::PtyAdapter, BotSpec, Capabilities, RuntimeAdapter, RuntimeSession, SessionEvent, + StartedSession, +}; +use anyhow::Context; +use std::sync::{Arc, Mutex}; + +pub struct NativeCodexAdapter; + +impl RuntimeAdapter for NativeCodexAdapter { + fn capabilities(&self) -> Capabilities { + CodexAdapter.capabilities() + } + fn probe(&self) -> anyhow::Result { + CodexAdapter.probe() + } + fn start(&self, spec: &BotSpec) -> anyhow::Result { + let remote = Remote::new()?; + let mut backend = start_server(spec, Some(&remote))?; + let codex = spec.codex.as_ref().context("missing Codex settings")?; + let thread = std::fs::read_to_string( + spec.workspace + .parent() + .unwrap_or(&spec.workspace) + .join("codex-thread-id"), + )?; + let mut terminal = spec.clone(); + terminal.claude_bin = super::super::executable::command(&codex.bin) + .get_program() + .to_str() + .context("Codex executable is not UTF-8")? + .to_string(); + terminal.claude_args = codex.args.clone(); + terminal.claude_args.extend([ + "resume".to_string(), + "--remote".to_string(), + remote.address, + "--remote-auth-token-env".to_string(), + "GRAVITY_CODEX_REMOTE_TOKEN".to_string(), + "--no-alt-screen".to_string(), + thread.trim().to_string(), + ]); + terminal + .env + .push(("GRAVITY_CODEX_REMOTE_TOKEN".to_string(), remote.token)); + let ui = match PtyAdapter.start(&terminal) { + Ok(ui) => ui, + Err(error) => { + let _ = backend.session.kill(); + return Err(error).context("start native Codex terminal"); + } + }; + let shared = Arc::new(Mutex::new(Sessions { + ui: ui.session, + backend: backend.session, + stopped: false, + exit_reported: false, + })); + let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); + for mut events in [ui.events, backend.events] { + let sessions = shared.clone(); + let out = tx.clone(); + std::thread::spawn(move || { + while let Some(event) = events.blocking_recv() { + if let SessionEvent::Exited { .. } = event { + let mut sessions = sessions.lock().unwrap_or_else(|e| e.into_inner()); + if !sessions.exit_reported { + sessions.exit_reported = true; + let _ = sessions.kill(); + let _ = out.send(event); + } + break; + } + if out.send(event).is_err() { + break; + } + } + }); + } + Ok(StartedSession { + session: Box::new(NativeSession(shared)), + events: rx, + msg_socket: None, + }) + } +} + +struct Sessions { + ui: Box, + backend: Box, + stopped: bool, + exit_reported: bool, +} + +impl Sessions { + fn kill(&mut self) -> anyhow::Result<()> { + if self.stopped { + return Ok(()); + } + self.stopped = true; + let ui = self.ui.kill(); + let backend = self.backend.kill(); + ui.and(backend) + } +} + +struct NativeSession(Arc>); +impl RuntimeSession for NativeSession { + fn send_input(&mut self, bytes: &[u8]) -> anyhow::Result<()> { + self.0 + .lock() + .unwrap_or_else(|e| e.into_inner()) + .ui + .send_input(bytes) + } + fn resize(&mut self, cols: u16, rows: u16) -> anyhow::Result<()> { + self.0 + .lock() + .unwrap_or_else(|e| e.into_inner()) + .ui + .resize(cols, rows) + } + fn deliver(&mut self, text: &str) -> Option> { + self.0 + .lock() + .unwrap_or_else(|e| e.into_inner()) + .backend + .deliver(text) + } + fn kill(&mut self) -> anyhow::Result<()> { + self.0.lock().unwrap_or_else(|e| e.into_inner()).kill() + } +} +impl Drop for NativeSession { + fn drop(&mut self) { + let _ = self.kill(); + } +} diff --git a/crates/gravityd/src/runtime/codex/transport.rs b/crates/gravityd/src/runtime/codex/transport.rs new file mode 100644 index 0000000..1b65a14 --- /dev/null +++ b/crates/gravityd/src/runtime/codex/transport.rs @@ -0,0 +1,118 @@ +//! A private, authenticated App Server shared by Gravity and the native TUI. +use super::Wire; +use anyhow::Context; +use futures::{SinkExt, StreamExt}; +use rand::RngCore; +use serde_json::Value; +use sha2::{Digest, Sha256}; +use std::io::{BufWriter, Write}; +use std::process::ChildStdin; +use std::sync::mpsc; +use std::time::{Duration, Instant}; +use tokio::sync::mpsc::UnboundedSender; +use tokio_tungstenite::tungstenite::{client::IntoClientRequest, Message}; + +pub(super) struct Remote { + pub address: String, + pub token: String, +} + +impl Remote { + pub fn new() -> anyhow::Result { + let reservation = std::net::TcpListener::bind("127.0.0.1:0")?; + let address = format!("ws://{}", reservation.local_addr()?); + let mut bytes = [0; 32]; + rand::thread_rng().fill_bytes(&mut bytes); + Ok(Self { + address, + token: hex::encode(bytes), + }) + } + + pub fn verifier(&self) -> String { + hex::encode(Sha256::digest(self.token.as_bytes())) + } +} + +pub(super) enum RpcWriter { + Stdio(BufWriter), + Socket(UnboundedSender), +} + +impl RpcWriter { + pub fn write(&mut self, value: &Value) -> anyhow::Result<()> { + match self { + Self::Stdio(input) => { + serde_json::to_writer(&mut *input, value)?; + input.write_all(b"\n")?; + input.flush()?; + } + Self::Socket(input) => input + .send(serde_json::to_string(value)?) + .map_err(|_| anyhow::anyhow!("Codex connection closed"))?, + } + Ok(()) + } +} + +pub(super) fn connect( + remote: &Remote, + tx: mpsc::Sender, + child: std::sync::Arc>, +) -> anyhow::Result { + let mut request = remote.address.clone().into_client_request()?; + request + .headers_mut() + .insert("Authorization", format!("Bearer {}", remote.token).parse()?); + let (out, mut outgoing) = tokio::sync::mpsc::unbounded_channel::(); + let (ready_tx, ready_rx) = mpsc::sync_channel(1); + std::thread::spawn(move || { + let mut run = || -> anyhow::Result<()> { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()?; + runtime.block_on(async { + let deadline = Instant::now() + Duration::from_secs(10); + let socket = loop { + if child.lock().unwrap_or_else(|e| e.into_inner()).try_wait()?.is_some() { + anyhow::bail!("Codex App Server exited before accepting connections"); + } + match tokio_tungstenite::connect_async(request.clone()).await { + Ok((socket, _)) => break socket, + Err(error) if Instant::now() >= deadline => return Err(error.into()), + Err(_) => tokio::time::sleep(Duration::from_millis(40)).await, + } + }; + let (mut sink, mut stream) = socket.split(); + let _ = ready_tx.send(Ok(())); + loop { + tokio::select! { + value = outgoing.recv() => { + let Some(value) = value else { break; }; + sink.send(Message::Text(value)).await?; + } + frame = stream.next() => { + match frame { + Some(Ok(Message::Text(text))) => { + if tx.send(Wire::Server(serde_json::from_str(&text)?)).is_err() { break; } + } + Some(Ok(Message::Close(_))) | None => break, + Some(Err(error)) => return Err(error.into()), + _ => {} + } + } + } + } + Ok(()) + }) + }; + if let Err(error) = run() { + let _ = ready_tx.send(Err(error)); + } + let _ = tx.send(Wire::Closed); + }); + ready_rx + .recv_timeout(Duration::from_secs(12)) + .context("connect to Codex App Server")??; + Ok(RpcWriter::Socket(out)) +} diff --git a/crates/gravityd/src/runtime/codex/worker.rs b/crates/gravityd/src/runtime/codex/worker.rs index d2ffe0c..8e3045f 100644 --- a/crates/gravityd/src/runtime/codex/worker.rs +++ b/crates/gravityd/src/runtime/codex/worker.rs @@ -2,15 +2,15 @@ use super::{approvals::Prompt, observations, transcript_path, BotSpec, SessionEv use anyhow::Context; use serde_json::{json, Value}; use std::collections::{BTreeMap, VecDeque}; -use std::io::{BufWriter, Write}; use std::path::PathBuf; -use std::process::{Child, ChildStdin}; +use std::process::Child; use std::sync::{mpsc, Arc, Mutex}; use std::time::{Duration, Instant}; pub(super) struct Worker { pub spec: BotSpec, - pub input: BufWriter, + pub input: super::transport::RpcWriter, + pub native: bool, pub rx: mpsc::Receiver, pub events: tokio::sync::mpsc::UnboundedSender, pub queued: VecDeque, @@ -28,7 +28,8 @@ pub(super) struct Worker { pub(super) fn run( spec: BotSpec, - stdin: ChildStdin, + input: super::transport::RpcWriter, + native: bool, rx: mpsc::Receiver, events: tokio::sync::mpsc::UnboundedSender, ready: mpsc::SyncSender>, @@ -37,7 +38,8 @@ pub(super) fn run( let transcript = transcript_path(&spec.workspace); let mut worker = Worker { spec, - input: BufWriter::new(stdin), + input, + native, rx, events, queued: VecDeque::new(), @@ -73,6 +75,9 @@ pub(super) fn run( impl Worker { pub fn output(&self, text: &str) { + if self.native { + return; + } let _ = self.events.send(SessionEvent::Output( text.replace('\n', "\r\n").into_bytes(), )); @@ -86,12 +91,9 @@ impl Worker { }); } pub fn write(&mut self, value: &Value) -> anyhow::Result<()> { - serde_json::to_writer(&mut self.input, value)?; - self.input.write_all(b"\n")?; - self.input.flush()?; - Ok(()) + self.input.write(value) } - fn rpc(&mut self, method: &str, params: Value) -> anyhow::Result { + pub fn rpc(&mut self, method: &str, params: Value) -> anyhow::Result { self.serial += 1; let id = self.serial; self.write(&json!({ "id": id, "method": method, "params": params }))?; @@ -136,7 +138,7 @@ impl Worker { } } fn initialize(&mut self) -> anyhow::Result<()> { - self.rpc("initialize", json!({ "clientInfo": { "name": "gravity", "title": "Gravity", "version": env!("CARGO_PKG_VERSION") } }))?; + self.rpc("initialize", json!({ "clientInfo": { "name": "gravity", "title": "Gravity", "version": env!("CARGO_PKG_VERSION") }, "capabilities": { "experimentalApi": true } }))?; self.write(&json!({ "method": "initialized", "params": {} }))?; let codex = self.spec.codex.as_ref().context("missing Codex settings")?; let mut roots = vec![self.spec.workspace.display().to_string()]; @@ -167,6 +169,9 @@ impl Worker { params["threadId"] = json!(id); "thread/resume" } else { + if self.native { + params["historyMode"] = json!("legacy"); + } "thread/start" }; let reply = match self.rpc(method, params.clone()) { @@ -179,6 +184,9 @@ impl Worker { if let Some(object) = params.as_object_mut() { object.remove("threadId"); } + if self.native { + params["historyMode"] = json!("legacy"); + } self.rpc("thread/start", params)? } result => result?, @@ -188,6 +196,13 @@ impl Worker { .and_then(Value::as_str) .context("Codex did not return a thread ID")? .to_string(); + if self.native { + // Naming materializes an empty thread without a model turn. + self.rpc( + "thread/name/set", + json!({ "threadId": self.thread, "name": self.spec.bot_name }), + )?; + } std::fs::write(path, format!("{}\n", self.thread))?; self.output("Codex CLI ready. Type a prompt and press Enter. /interrupt stops a turn.\n"); self.hook("SessionStart", None); @@ -213,8 +228,10 @@ impl Worker { } } } - fn deliver(&mut self, text: &str) -> anyhow::Result<()> { - observations::append(&self.transcript, "user", text)?; + pub fn deliver(&mut self, text: &str) -> anyhow::Result<()> { + if !self.native { + observations::append(&self.transcript, "user", text)?; + } let input = json!([{ "type": "text", "text": text }]); if let Some(turn) = &self.turn { match self.rpc( @@ -244,61 +261,6 @@ impl Worker { } Ok(()) } - fn keystrokes(&mut self, bytes: &[u8]) -> anyhow::Result<()> { - for &byte in bytes { - if self.escaping { - if byte != b'[' && (0x40..=0x7e).contains(&byte) { - self.escaping = false; - } - continue; - } - match byte { - 27 => self.escaping = true, - 3 => { - self.line.clear(); - self.interrupt()?; - } - 8 | 127 => { - if let Some(last) = self.line.pop() { - if last & 0xc0 == 0x80 { - while self.line.pop().is_some_and(|b| b & 0xc0 == 0x80) {} - } - self.output("\u{8} \u{8}"); - } - } - 13 | 10 if !self.line.is_empty() => { - let text = String::from_utf8(std::mem::take(&mut self.line)) - .context("prompt is not UTF-8")?; - self.output("\n"); - if text == "/interrupt" { - self.interrupt()?; - } else if !self.answer(&text)? { - self.deliver(&text)?; - } - } - 13 | 10 => {} - b if b >= 32 => self.line.push(b), - _ => {} - } - } - // Echo whole UTF-8 inputs rather than one byte per output frame. - if !bytes - .iter() - .any(|b| matches!(b, 3 | 8 | 10 | 13 | 27 | 127)) - { - let _ = self.events.send(SessionEvent::Output(bytes.to_vec())); - } - Ok(()) - } - fn interrupt(&mut self) -> anyhow::Result<()> { - if let Some(turn) = &self.turn { - self.rpc( - "turn/interrupt", - json!({ "threadId": self.thread, "turnId": turn }), - )?; - } - Ok(()) - } fn server(&mut self, value: Value) -> anyhow::Result<()> { let Some(method) = value.get("method").and_then(Value::as_str) else { return Ok(()); @@ -311,6 +273,14 @@ impl Worker { ); } let params = value.get("params").unwrap_or(&Value::Null); + if self.native + && params + .get("threadId") + .and_then(Value::as_str) + .is_some_and(|id| !self.thread.is_empty() && id != self.thread) + { + return Ok(()); + } match method { "turn/started" => { self.turn = params @@ -346,6 +316,18 @@ impl Worker { } } "item/completed" => { + if self.native + && params.pointer("/item/type").and_then(Value::as_str) == Some("userMessage") + { + if let Some(content) = params.pointer("/item/content").and_then(Value::as_array) + { + for item in content { + if let Some(text) = item.get("text").and_then(Value::as_str) { + observations::append(&self.transcript, "user", text)?; + } + } + } + } if params.pointer("/item/type").and_then(Value::as_str) == Some("agentMessage") { if let Some(text) = params.pointer("/item/text").and_then(Value::as_str) { observations::append(&self.transcript, "assistant", text)?; diff --git a/crates/gravityd/src/runtime/mixed.rs b/crates/gravityd/src/runtime/mixed.rs index 09ebf37..b08bd54 100644 --- a/crates/gravityd/src/runtime/mixed.rs +++ b/crates/gravityd/src/runtime/mixed.rs @@ -1,4 +1,4 @@ -use super::codex::CodexAdapter; +use super::codex::NativeCodexAdapter; use super::pty::PtyAdapter; use super::{BotSpec, Capabilities, RuntimeAdapter, StartedSession}; use crate::config::Config; @@ -37,7 +37,7 @@ impl RuntimeAdapter for MixedAdapter { } fn start(&self, spec: &BotSpec) -> anyhow::Result { if spec.codex.is_some() { - CodexAdapter.start(spec) + NativeCodexAdapter.start(spec) } else { PtyAdapter.start(spec) } diff --git a/crates/gravityd/src/runtime/pty.rs b/crates/gravityd/src/runtime/pty.rs index 49d9e7a..e238129 100644 --- a/crates/gravityd/src/runtime/pty.rs +++ b/crates/gravityd/src/runtime/pty.rs @@ -59,7 +59,7 @@ impl RuntimeAdapter for PtyAdapter { // (CLAUDECODE, CLAUDE_CODE_*) would make the bot think it is a nested // child session and, e.g., disable transcript saving. for (key, _) in std::env::vars() { - if key == "CLAUDECODE" || key.starts_with("CLAUDE_CODE_") { + if key == "CODEX_THREAD_ID" || key == "CLAUDECODE" || key.starts_with("CLAUDE_CODE_") { cmd.env_remove(&key); } } @@ -68,7 +68,10 @@ impl RuntimeAdapter for PtyAdapter { } cmd.env("TERM", "xterm-256color"); - let child = pair.slave.spawn_command(cmd).context("spawn claude")?; + let child = pair + .slave + .spawn_command(cmd) + .context("spawn terminal CLI")?; drop(pair.slave); let mut reader = pair.master.try_clone_reader().context("clone pty reader")?; diff --git a/crates/gravityd/tests/codex_native.rs b/crates/gravityd/tests/codex_native.rs index 616b9e7..3d2579c 100644 --- a/crates/gravityd/tests/codex_native.rs +++ b/crates/gravityd/tests/codex_native.rs @@ -1,6 +1,6 @@ -//! Opt-in handshake against a locally installed Codex CLI; no model turn is sent. -use gravityd::runtime::codex::{CodexAdapter, CodexSpec}; -use gravityd::runtime::{BotSpec, RuntimeAdapter}; +//! Opt-in tests of the installed Codex CLI; model turns use only a local fixture. +use gravityd::runtime::codex::{CodexAdapter, CodexSpec, NativeCodexAdapter}; +use gravityd::runtime::{BotSpec, RuntimeAdapter, SessionEvent}; #[test] #[ignore = "requires CODEX_BINARY pointing to a locally installed CLI"] @@ -38,3 +38,141 @@ fn native_codex_initializes_and_resumes_without_a_model_request() { let mut resumed = CodexAdapter.start(&spec).expect("native thread resume"); resumed.session.kill().unwrap(); } + +#[tokio::test] +#[ignore = "requires CODEX_BINARY; native terminal with local mock Responses"] +async fn native_terminal_renders_the_official_codex_interface() { + use std::io::BufRead; + let mut provider = LocalProvider( + std::process::Command::new(std::env::var("NODE_BINARY").unwrap_or_else(|_| "node".into())) + .arg( + std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/codex-responses.mjs"), + ) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::null()) + .spawn() + .unwrap(), + ); + let mut endpoint = String::new(); + std::io::BufReader::new(provider.0.stdout.take().unwrap()) + .read_line(&mut endpoint) + .unwrap(); + let root = tempfile::tempdir().unwrap(); + let workspace = root.path().join("workspace"); + std::fs::create_dir_all(&workspace).unwrap(); + let home = root.path().join("codex-home"); + std::fs::create_dir_all(&home).unwrap(); + let spec = BotSpec { + bot_id: "native-tui".into(), + bot_name: "Native TUI".into(), + workspace, + claude_bin: "unused".into(), + claude_args: Vec::new(), + codex: Some(CodexSpec { + bin: std::env::var("CODEX_BINARY").unwrap(), + args: vec!["-c".into(), "model_provider=\"gravity_test\"".into(), "-c".into(), format!("model_providers.gravity_test={{name=\"Local test\",base_url=\"{}\",wire_api=\"responses\",requires_openai_auth=false}}", endpoint.trim())], + port: 1, + artifacts: None, + }), + env: vec![("CODEX_HOME".into(), home.display().to_string()), ("NO_PROXY".into(), "127.0.0.1,localhost".into())], + cols: 100, + rows: 30, + }; + let mut started = NativeCodexAdapter.start(&spec).unwrap(); + let mut output = String::new(); + let rendered = tokio::time::timeout(std::time::Duration::from_secs(20), async { + loop { + match started.events.recv().await.unwrap() { + SessionEvent::Output(bytes) => { + output.push_str(&String::from_utf8_lossy(&bytes)); + if bytes.windows(4).any(|chunk| chunk == b"\x1b[6n") { + started.session.send_input(b"\x1b[1;1R").unwrap(); + } + if output.contains("OpenAI Codex") && output.contains("/review") { + break; + } + } + SessionEvent::Exited { code } => panic!("native TUI exited {code:?}: {output}"), + _ => {} + } + } + }) + .await; + if rendered.is_ok() { + started + .session + .deliver("native bus smoke") + .unwrap() + .unwrap(); + let reply = tokio::time::timeout(std::time::Duration::from_secs(20), async { + loop { + match started.events.recv().await.unwrap() { + SessionEvent::Output(bytes) => { + output.push_str(&String::from_utf8_lossy(&bytes)) + } + SessionEvent::Lifecycle { + event: "Stop" | "TurnInterrupted", + .. + } => break, + SessionEvent::Exited { code } => panic!("native exit {code:?}: {output}"), + _ => {} + } + } + }) + .await; + tokio::time::sleep(std::time::Duration::from_millis(200)).await; + while let Ok(SessionEvent::Output(bytes)) = started.events.try_recv() { + output.push_str(&String::from_utf8_lossy(&bytes)); + } + assert!(reply.is_ok(), "local response timeout: {output}"); + started + .session + .send_input(b"native keyboard smoke") + .unwrap(); + tokio::time::sleep(std::time::Duration::from_millis(250)).await; + started.session.send_input(b"\r").unwrap(); + tokio::time::timeout(std::time::Duration::from_secs(15), async { + loop { + match started.events.recv().await.unwrap() { + SessionEvent::Lifecycle { event: "Stop", .. } => break, + SessionEvent::Exited { code } => panic!("native exit {code:?}"), + SessionEvent::Output(bytes) => { + output.push_str(&String::from_utf8_lossy(&bytes)) + } + _ => {} + } + } + }) + .await + .unwrap_or_else(|error| panic!("native keyboard turn: {error}: {output}")); + started.session.kill().unwrap(); + let transcript = + std::fs::read_to_string(root.path().join("codex-observations.jsonl")).unwrap(); + assert!( + transcript.contains("Local response: native bus smoke"), + "{transcript}\n{output}" + ); + assert!( + transcript.contains("Local response: native keyboard smoke"), + "{transcript}" + ); + drop(started); + let mut resumed = NativeCodexAdapter + .start(&spec) + .expect("persisted native conversation resumes"); + resumed.session.kill().unwrap(); + } else { + started.session.kill().unwrap(); + } + assert!(rendered.is_ok(), "native interface timed out: {output}"); + assert!(!output.contains("Codex CLI ready. Type a prompt")); +} + +struct LocalProvider(std::process::Child); +impl Drop for LocalProvider { + fn drop(&mut self) { + let _ = self.0.kill(); + let _ = self.0.wait(); + } +} diff --git a/crates/gravityd/tests/codex_runtime.rs b/crates/gravityd/tests/codex_runtime.rs index 0685228..84a7003 100644 --- a/crates/gravityd/tests/codex_runtime.rs +++ b/crates/gravityd/tests/codex_runtime.rs @@ -1,4 +1,4 @@ -use gravityd::runtime::codex::{CodexAdapter, CodexSpec}; +use gravityd::runtime::codex::{CodexAdapter, CodexSpec, NativeCodexAdapter}; use gravityd::runtime::{BotSpec, RuntimeAdapter, SessionEvent, StartedSession}; use serde_json::Value; use std::path::Path; @@ -35,6 +35,70 @@ fn spec(root: &Path) -> BotSpec { } } +#[tokio::test] +async fn native_terminal_and_bus_share_history_without_consuming_a_draft() { + let root = tempfile::tempdir().unwrap(); + let spec = spec(root.path()); + let mut started = NativeCodexAdapter.start(&spec).unwrap(); + tokio::time::timeout(Duration::from_secs(5), async { + loop { + match started.events.recv().await.unwrap() { + SessionEvent::Output(bytes) + if String::from_utf8_lossy(&bytes).contains("Native Codex CLI ready") => + { + break + } + SessionEvent::Exited { code } => panic!("native terminal exit {code:?}"), + _ => {} + } + } + }) + .await + .unwrap(); + started.session.resize(120, 40).unwrap(); + started.session.send_input("draft 🪟".as_bytes()).unwrap(); + started.session.deliver("bus envelope").unwrap().unwrap(); + hook(&mut started, "Stop").await; + started.session.send_input(b"\r").unwrap(); + hook(&mut started, "Stop").await; + let turns: Vec<_> = log(root.path()) + .into_iter() + .filter(|v| v["method"] == "turn/start") + .collect(); + assert_eq!(turns[0]["params"]["input"][0]["text"], "bus envelope"); + assert_eq!(turns[1]["params"]["input"][0]["text"], "draft 🪟"); + let observations = + std::fs::read_to_string(root.path().join("codex-observations.jsonl")).unwrap(); + assert_eq!(observations.matches("bus envelope").count(), 1); + assert!(observations.contains("draft 🪟")); + started.session.deliver("approval").unwrap().unwrap(); + hook(&mut started, "Notification").await; + assert!(!log(root.path()) + .iter() + .any(|v| v["id"] == "approval-1" && v.get("result").is_some())); + started.session.send_input(b"n").unwrap(); + hook(&mut started, "Stop").await; + assert!(log(root.path()) + .iter() + .any(|v| v["id"] == "approval-1" && v["result"]["decision"] == "decline")); + started.session.send_input(b"/exit\r").unwrap(); + tokio::time::timeout(Duration::from_secs(5), async { + loop { + if let Some(SessionEvent::Exited { .. }) = started.events.recv().await { + break; + } + } + }) + .await + .unwrap(); + started.session.kill().unwrap(); + let mut resumed = NativeCodexAdapter.start(&spec).unwrap(); + assert!(log(root.path()) + .iter() + .any(|v| v["method"] == "thread/resume")); + resumed.session.kill().unwrap(); +} + fn log(root: &Path) -> Vec { std::fs::read_to_string(root.join("rpc.jsonl")) .unwrap() diff --git a/crates/gravityd/tests/fixtures/codex-responses.mjs b/crates/gravityd/tests/fixtures/codex-responses.mjs new file mode 100644 index 0000000..801aef5 --- /dev/null +++ b/crates/gravityd/tests/fixtures/codex-responses.mjs @@ -0,0 +1,37 @@ +// Deterministic Responses provider for opt-in tests of the real Codex binary. +import { createServer } from "node:http"; +import { gunzipSync } from "node:zlib"; +let serial = 0; +const server = createServer(async (request, response) => { + try { + const chunks = []; + for await (const chunk of request) { chunks.push(chunk); } + let bytes = Buffer.concat(chunks); + if (request.headers["content-encoding"] === "gzip") { bytes = gunzipSync(bytes); } + if (!request.url.endsWith("/responses")) { + response.writeHead(200, { "Content-Type": "application/json" }); + response.end(JSON.stringify({ data: [] })); + return; + } + const input = JSON.parse(bytes.toString()); + const users = input.input.filter((item) => item.role === "user"); + const text = users.at(-1)?.content?.map((item) => item.text ?? "").join("") ?? ""; + const called = input.input.some((item) => item.type === "function_call_output"); + const shell = (input.tools ?? []).find((tool) => ["exec_command", "shell_command"].includes(tool.name)) ?? { name: "exec_command" }; + const id = `local-${++serial}`; + const events = [{ type: "response.created", response: { id } }]; + if (text.includes("approval") && !called && shell) { + const args = shell.name === "exec_command" ? { cmd: "echo local-test", sandbox_permissions: "require_escalated", justification: "Local test of native approval" } : { command: "echo local-test", sandbox_permissions: "require_escalated", justification: "Local test of native approval" }; + events.push({ type: "response.output_item.done", item: { type: "function_call", call_id: "local-command", name: shell.name, arguments: JSON.stringify(args) } }); + } else { + events.push({ type: "response.output_item.done", item: { type: "message", role: "assistant", id: "local-message", content: [{ type: "output_text", text: `Local response: ${text}` }] } }); + } + events.push({ type: "response.completed", response: { id, usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 } } }); + response.writeHead(200, { "Content-Type": "text/event-stream" }); + response.end(events.map((event) => `event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`).join("")); + } catch (error) { + response.writeHead(400, { "Content-Type": "application/json" }); + response.end(JSON.stringify({ error: { message: `${request.headers["content-encoding"]}: ${error.message}` } })); + } +}); +server.listen(0, "127.0.0.1", () => { console.log(`http://127.0.0.1:${server.address().port}/v1`); }); diff --git a/crates/gravityd/tests/fixtures/codex-server.mjs b/crates/gravityd/tests/fixtures/codex-server.mjs index e84b193..a3912f4 100644 --- a/crates/gravityd/tests/fixtures/codex-server.mjs +++ b/crates/gravityd/tests/fixtures/codex-server.mjs @@ -1,23 +1,26 @@ import { appendFileSync } from "node:fs"; import { createInterface } from "node:readline"; +import { listen, terminal } from "./codex-websocket.mjs"; const log = process.env.GRAVITY_TEST_LOG ?? "../codex-fixture-rpc.jsonl"; -const send = (value) => { process.stdout.write(`${JSON.stringify(value)}\n`); }; -const reply = (id, result) => { send({ id, result }); }; +let send = (value) => { process.stdout.write(`${JSON.stringify(value)}\n`); }; const notify = (method, params) => { send({ method, params }); }; let active = false; -for await (const line of createInterface({ input: process.stdin })) { - const request = JSON.parse(line); +function receive(request, respond) { + const line = JSON.stringify(request); + const reply = (id, result) => { respond({ id, result }); }; appendFileSync(log, `${line}\n`); const { id, method, params } = request; if (method === "initialize") { reply(id, { userAgent: "fixture" }); } else if (method === "initialized") { /* notification */ } - else if (method === "thread/start" || method === "thread/resume") { + else if (method === "thread/name/set") { reply(id, {}); } + else if (method === "thread/start" || method === "thread/resume" || method === "thread/read") { reply(id, { thread: { id: "thread-fixture" } }); } else if (method === "turn/start") { const text = params.input[0].text; active = true; notify("turn/started", { turn: { id: "turn-fixture", status: "inProgress" } }); + notify("item/completed", { item: { type: "userMessage", content: [{ type: "text", text }] } }); reply(id, { turn: { id: "turn-fixture", status: "inProgress" } }); if (text === "approval") { send({ id: "approval-1", method: "item/commandExecution/requestApproval", params: { command: "echo hello", threadId: "thread-fixture", turnId: "turn-fixture" } }); @@ -39,3 +42,12 @@ for await (const line of createInterface({ input: process.stdin })) { notify("turn/completed", { turn: { id: "turn-fixture", status: "completed" } }); } else { throw new Error(`Unexpected request ${line}`); } } + +const args = process.argv.slice(2); +if (args.includes("--remote")) { + await terminal(args[args.indexOf("--remote") + 1], process.env.GRAVITY_CODEX_REMOTE_TOKEN); +} else if (args.includes("--ws-auth")) { + send = listen(args[args.indexOf("--listen") + 1], args[args.indexOf("--ws-token-sha256") + 1], receive); +} else { + for await (const line of createInterface({ input: process.stdin })) { receive(JSON.parse(line), send); } +} diff --git a/crates/gravityd/tests/fixtures/codex-websocket.mjs b/crates/gravityd/tests/fixtures/codex-websocket.mjs new file mode 100644 index 0000000..9935ba7 --- /dev/null +++ b/crates/gravityd/tests/fixtures/codex-websocket.mjs @@ -0,0 +1,123 @@ +// Small local JSON WebSocket fixture; no external service or dependencies. +import { createServer } from "node:http"; +import { createHash } from "node:crypto"; +import { createConnection } from "node:net"; + +export function listen(address, verifier, receive) { + const peers = new Set(); + const server = createServer(); + server.on("upgrade", (request, socket) => { + const token = request.headers.authorization?.replace(/^Bearer /, "") ?? ""; + if (createHash("sha256").update(token).digest("hex") !== verifier) { + socket.end("HTTP/1.1 401 Unauthorized\r\n\r\n"); + return; + } + const accept = createHash("sha1").update(request.headers["sec-websocket-key"] + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").digest("base64"); + socket.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${accept}\r\n\r\n`); + const send = (value) => { + const body = Buffer.from(JSON.stringify(value)); + const header = Buffer.alloc(body.length < 126 ? 2 : 4); + header[0] = 0x81; + header[1] = body.length < 126 ? body.length : 126; + if (body.length >= 126) { header.writeUInt16BE(body.length, 2); } + socket.write(Buffer.concat([header, body])); + }; + peers.add(send); + socket.on("close", () => { peers.delete(send); }); + socket.on("error", () => { peers.delete(send); }); + let pending = Buffer.alloc(0); + socket.on("data", (bytes) => { + pending = Buffer.concat([pending, bytes]); + while (pending.length >= 2) { + const opcode = pending[0] & 15; + let size = pending[1] & 127; + let offset = 2; + if (size === 126) { + if (pending.length < 4) { return; } + size = pending.readUInt16BE(2); offset = 4; + } + if (size === 127) { throw new Error("fixture message too large"); } + const masked = Boolean(pending[1] & 128); + const start = offset + (masked ? 4 : 0); + if (pending.length < start + size) { return; } + const body = Buffer.from(pending.subarray(start, start + size)); + if (masked) { + for (let i = 0; i < size; i++) { body[i] ^= pending[offset + i % 4]; } + } + pending = pending.subarray(start + size); + if (opcode === 8) { socket.end(); return; } + if (opcode === 1) { receive(JSON.parse(body.toString()), send); } + } + }); + }); + server.listen(Number(new URL(address).port), "127.0.0.1"); + return (value) => { for (const send of peers) { send(value); } }; +} + +export async function terminal(address, token) { + const endpoint = new URL(address); + const socket = createConnection(Number(endpoint.port), "127.0.0.1"); + await new Promise((resolve, reject) => { + socket.once("connect", resolve); + socket.once("error", reject); + }); + socket.write(`GET / HTTP/1.1\r\nHost: ${endpoint.host}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: Z3Jhdml0eS10ZXN0LWtleQ==\r\nSec-WebSocket-Version: 13\r\nAuthorization: Bearer ${token}\r\n\r\n`); + await new Promise((resolve, reject) => { + socket.once("data", (bytes) => { + if (bytes.toString().startsWith("HTTP/1.1 101")) { resolve(); } + else { reject(new Error("WebSocket upgrade failed")); } + }); + }); + const write = (value) => { + const body = Buffer.from(JSON.stringify(value)); + const header = Buffer.alloc(body.length < 126 ? 6 : 8); + header[0] = 0x81; header[1] = 128 | (body.length < 126 ? body.length : 126); + if (body.length >= 126) { header.writeUInt16BE(body.length, 2); } + // A zero mask is legal; these test messages contain no credentials. + socket.write(Buffer.concat([header, body])); + }; + let serial = 100; + const send = (method, params) => { write({ id: serial++, method, params }); }; + send("initialize", { clientInfo: { name: "fixture-tui" } }); + send("thread/resume", { threadId: "thread-fixture" }); + process.stdout.write("\x1b[32mNative Codex CLI ready\x1b[0m\r\n› "); + process.stdin.setRawMode(true); + let draft = []; + let approval; + let pending = Buffer.alloc(0); + const receive = (message) => { + if (message.method === "item/agentMessage/delta") { process.stdout.write(message.params.delta); } + if (message.method === "item/commandExecution/requestApproval") { + approval = message.id; + process.stdout.write("\r\nApprove command? y/n\r\n"); + } + }; + socket.on("data", (bytes) => { + pending = Buffer.concat([pending, bytes]); + while (pending.length >= 2) { + let size = pending[1] & 127; + let offset = 2; + if (size === 126) { + if (pending.length < 4) { return; } + size = pending.readUInt16BE(2); offset = 4; + } + if (pending.length < offset + size) { return; } + receive(JSON.parse(pending.subarray(offset, offset + size).toString())); + pending = pending.subarray(offset + size); + } + }); + process.stdin.on("data", (bytes) => { + for (const byte of bytes) { + if (approval && (byte === 110 || byte === 121)) { + write({ id: approval, result: { decision: byte === 110 ? "decline" : "accept" } }); + approval = undefined; + } else if (byte === 3) { send("turn/interrupt", { threadId: "thread-fixture", turnId: "turn-fixture" }); } + else if (byte === 13) { + const text = Buffer.from(draft).toString(); + if (text === "/exit") { process.exit(0); } + send("turn/start", { threadId: "thread-fixture", input: [{ type: "text", text }] }); + draft = []; + } else { draft.push(byte); } + } + }); +} diff --git a/crates/gravityd/tests/windows_pty.rs b/crates/gravityd/tests/windows_pty.rs index 40821f4..1158742 100644 --- a/crates/gravityd/tests/windows_pty.rs +++ b/crates/gravityd/tests/windows_pty.rs @@ -164,6 +164,18 @@ async fn check_provider_switch(codex_bin: String, codex_args: Vec) { .await .expect("Codex starts after terminating the native PTY"); let terminal = daemon.app.supervisor.term(id).unwrap(); + tokio::time::timeout(Duration::from_secs(20), async { + while !terminal + .replay_after(0) + .frames + .iter() + .any(|frame| String::from_utf8_lossy(&frame.data).contains("Codex")) + { + tokio::time::sleep(Duration::from_millis(20)).await; + } + }) + .await + .expect("native Codex terminal renders"); let replay: Vec = terminal .replay_after(0) .frames @@ -174,7 +186,7 @@ async fn check_provider_switch(codex_bin: String, codex_args: Vec) { let reset = output .rfind("\x1bc") .expect("clear the previous provider's screen"); - assert!(output[reset..].contains("Codex CLI ready"), "{output}"); + assert!(output[reset..].contains("Codex"), "{output}"); let reply = client .request(json!({"type":"set_bot_runtime", "bot_id":id, "runtime":"claude_code"})) .await; diff --git a/docs/bot-runtimes.md b/docs/bot-runtimes.md index e446036..92a6950 100644 --- a/docs/bot-runtimes.md +++ b/docs/bot-runtimes.md @@ -26,18 +26,23 @@ setting continues to select the deterministic test adapter for every bot. ## Codex terminal Codex runs through [Codex App Server](https://learn.chatgpt.com/docs/app-server) -over stdio. Tested against Codex CLI 0.159.2. The terminal displays streamed text -and command output; type a prompt and press Enter. `/interrupt` or Ctrl+C cancels -the active turn. Bus messages arrive through structured turn requests and never -consume a partially typed terminal prompt. Terminal resize is cosmetic: this -adapter does not render the Codex full-screen CLI interface. - -Approval requests appear with numbered prompts. `/approve 1` grants the requested -command, file change or permissions; `/deny 1` declines it. Permissions are granted -only for the current turn. Requests are never automatically approved. Questions use -`/answer 1 {"answers":{"question-id":{"answers":["your answer"]}}}`. MCP elicitation -uses `/answer 1 {"action":"accept","content":{"field":"value"}}`; `/deny 1` declines it. -Unsupported server requests fail explicitly. +over a private authenticated loopback WebSocket, with the native Codex CLI terminal +attached to the same thread. Tested against Codex CLI 0.159.3; the CLI must support +`--remote` and App Server capability-token authentication. Codex's prompt editor, +status, slash commands, keyboard shortcuts, approval dialogs and questions appear +directly in Gravity's terminal. Terminal resize reaches the CLI; inline mode keeps +scrollback available. Use the native CLI controls to interrupt or leave a session. +Gravity supervision restarts a bot after its terminal exits. + +Bus messages arrive through structured turn requests and never consume a partially +typed terminal prompt. Gravity observes both native and bus turns for activity and +routine completion. The native CLI handles approval decisions; Gravity does not +automatically answer them. Each bot has a separate App Server with a fresh random +capability token. The raw token is passed through the terminal environment and RPC +authorization header; only its SHA-256 verifier appears in server arguments. + +The upstream WebSocket transport and remote terminal are experimental. Keep the +runtime contribution in draft until live provider validation is complete. Each bot receives its own Gravity bus token through an environment variable; the App Server config enables the local Gravity MCP endpoint. Codex uses workspace-write @@ -51,5 +56,17 @@ Interrupted or failed turns do not report successful routine completion. The additive SQLite migration sets existing bots to `claude_code`. Update the daemon and client together to expose the picker; the client hides it for older daemons. Keep a database backup before upgrading, as older daemons do not understand -Codex bots. CLI authentication and a live model response require the user's account; -the test suite uses a local App Server fixture without network or model charges. +Codex bots. CLI authentication and a live model response require the user's account. +The default tests use a local App Server fixture. Opt-in native CLI tests use a +local mock Responses provider without paid model requests. + +For a development installer with the same version number as the installed build, +refresh the managed daemon explicitly after installing: + +```powershell +.\gravityd.exe service install +``` + +Use the daemon built with that installer. Running its `.exe` without `service +install` starts a foreground daemon and does not replace the managed service. The +refresh restarts bot sessions while preserving projects and configuration. diff --git a/third-party/inventory.json b/third-party/inventory.json index 77b24db..57fbffd 100644 --- a/third-party/inventory.json +++ b/third-party/inventory.json @@ -6,7 +6,7 @@ "inputs": { "Cargo.lock": "ed089d7f431124cb093bc49f5c54a6ca4cb0af573ce8be2f6386475d742fca84", "Cargo.toml": "17f46fbe0ce2a8fab2e1b26a1b10435ac1968ae6f8150db33bb1e05c25fb7491", - "crates/gravityd/Cargo.toml": "ae8213b9576b7ce6baa6efda1a6e8633dcbc0888bbd96f60383323ffa81702fd", + "crates/gravityd/Cargo.toml": "94c7560d2df9a9f0c377739a0f858b6af099db9ff5de3789f6e1876e0169ad8e", "crates/bus/Cargo.toml": "c4c4f51131d78f8ae8cb6e96fcb918c245998d9c1e01312f513fb56dfc452910", "apps/desktop/src-tauri/Cargo.lock": "7c73bbce32b88bd02862bcf73cea344288764824065d4379787d9c0de7991474", "apps/desktop/src-tauri/Cargo.toml": "421dc0d9bcf49e5d93eb01a987ef7b77b393e1e383be7eac64cabde2daf53702", From 52ee60f0a4ba3c1d1b0988aa35145d7501728e54 Mon Sep 17 00:00:00 2001 From: Manuel Rinaldi Date: Thu, 1 Oct 2026 13:58:05 +0300 Subject: [PATCH 08/55] fix(mcp): expose bot runtime selection and verification --- crates/gravityd/src/botmgmt.rs | 4 + crates/gravityd/src/botmgmt/runtime.rs | 46 ++++ crates/gravityd/src/mcp/schema.rs | 12 +- crates/gravityd/src/mcp/selfmgmt.rs | 20 +- crates/gravityd/src/mcp/tools.rs | 1 + crates/gravityd/src/ws/entities.rs | 3 +- crates/gravityd/src/ws/runtime.rs | 30 +-- crates/gravityd/tests/mcp_runtime.rs | 280 +++++++++++++++++++++++++ docs/bot-runtimes.md | 12 +- 9 files changed, 378 insertions(+), 30 deletions(-) create mode 100644 crates/gravityd/src/botmgmt/runtime.rs create mode 100644 crates/gravityd/tests/mcp_runtime.rs diff --git a/crates/gravityd/src/botmgmt.rs b/crates/gravityd/src/botmgmt.rs index 9c6f1b5..ca2fb7c 100644 --- a/crates/gravityd/src/botmgmt.rs +++ b/crates/gravityd/src/botmgmt.rs @@ -20,10 +20,14 @@ mod archive; mod charter; mod create; mod revert; +mod runtime; pub use archive::{archive_bot, prune_archived_workspaces}; pub use create::{create_bot, create_bot_with_runtime, Created}; pub use revert::revert_revision; +pub use runtime::{ + check_runtime_available, requested_runtime, set_bot_runtime, RuntimeUnavailable, +}; /// Fields that may change on a bot. `None` leaves the stored value alone, /// which is what lets `update_self(avatar: "…")` avoid touching instructions. diff --git a/crates/gravityd/src/botmgmt/runtime.rs b/crates/gravityd/src/botmgmt/runtime.rs new file mode 100644 index 0000000..5ac3e91 --- /dev/null +++ b/crates/gravityd/src/botmgmt/runtime.rs @@ -0,0 +1,46 @@ +//! Runtime selection shared by the control client and bot-authenticated MCP. + +use std::sync::Arc; + +use bus::{Bot, BotRuntime}; +use serde_json::Value; + +use crate::app::AppState; +use crate::events::Push; + +#[derive(Debug, thiserror::Error)] +#[error("{0}")] +pub struct RuntimeUnavailable(String); + +pub fn requested_runtime(args: &Value) -> anyhow::Result> { + args.get("runtime") + .map(|value| serde_json::from_value(value.clone()).map_err(anyhow::Error::from)) + .transpose() +} + +/// Preflight before changing any settings or stopping a working session. +pub fn check_runtime_available(app: &Arc, runtime: BotRuntime) -> anyhow::Result<()> { + app.supervisor + .adapter() + .check_available(runtime) + .map_err(|error| RuntimeUnavailable(format!("{error:#}")).into()) +} + +/// Persist a provider change and restart, preserving the workspace and histories. +pub fn set_bot_runtime(app: &Arc, bot: &Bot, runtime: BotRuntime) -> anyhow::Result { + if bot.runtime == runtime { + return Ok(bot.clone()); + } + check_runtime_available(app, runtime)?; + app.db.set_bot_runtime(&bot.id, runtime)?; + if let Err(error) = app.supervisor.restart_bot(&bot.id) { + app.db.set_bot_runtime(&bot.id, bot.runtime)?; + return Err(error); + } + let mut updated = bot.clone(); + updated.runtime = runtime; + app.events.push(Push::BotUpdated { + bot: updated.clone(), + }); + Ok(updated) +} diff --git a/crates/gravityd/src/mcp/schema.rs b/crates/gravityd/src/mcp/schema.rs index bcd9320..162f3f6 100644 --- a/crates/gravityd/src/mcp/schema.rs +++ b/crates/gravityd/src/mcp/schema.rs @@ -62,7 +62,7 @@ fn core_tools() -> Vec { }), vec!["task_id"]), tool("list_bots", - "List bots in your project with status. The id is what a signal \ + "List bots in your project with status and runtime (claude_code or codex_cli). The id is what a signal \ trigger's from_bot_id refers to.", json!({}), vec![]), tool("check_inbox", @@ -117,7 +117,7 @@ fn core_tools() -> Vec { json!({ "routine_id": {"type": "string"} }), vec!["routine_id"]), tool("get_self", - "Read your own settings: name, avatar, description, instructions, who created you, and the bots you created.", + "Read your own settings: name, avatar, description, instructions, runtime (claude_code or codex_cli), who created you, and the bots you created.", json!({}), vec![]), tool("update_self", "Change your own avatar, description or instructions. Applies immediately; omitted fields are left alone.", @@ -137,16 +137,18 @@ fn core_tools() -> Vec { "name": {"type": "string"}, "description": {"type": "string", "description": "Optional. Short summary other bots see in list_bots"}, "instructions": {"type": "string", "description": "Optional. Standing instructions for the new bot"}, - "avatar": {"type": "string", "description": format!("Optional; defaults to a random icon. {}", avatar_hint())} + "avatar": {"type": "string", "description": format!("Optional; defaults to a random icon. {}", avatar_hint())}, + "runtime": {"type": "string", "enum": ["claude_code", "codex_cli"], "description": "Optional. Select Claude Code or Codex CLI explicitly; omitted inherits your runtime. The result reports the saved runtime; list_bots verifies it."} }), vec!["name"]), tool("update_bot", - "Change the avatar, description or instructions of a bot you created.", + "Change the avatar, description, instructions or runtime of a bot you created. Omitted fields stay unchanged. Changing runtime interrupts its current turn and restarts its session, preserving the workspace and separate provider histories.", json!({ "name": {"type": "string", "description": "The bot to change"}, "description": {"type": "string"}, "instructions": {"type": "string"}, - "avatar": {"type": "string", "description": avatar_hint()} + "avatar": {"type": "string", "description": avatar_hint()}, + "runtime": {"type": "string", "enum": ["claude_code", "codex_cli"], "description": "Optional. Select Claude Code or Codex CLI. The result reports the saved runtime; list_bots verifies it."} }), vec!["name"]), tool("delete_bot", diff --git a/crates/gravityd/src/mcp/selfmgmt.rs b/crates/gravityd/src/mcp/selfmgmt.rs index bfaa849..aa61ade 100644 --- a/crates/gravityd/src/mcp/selfmgmt.rs +++ b/crates/gravityd/src/mcp/selfmgmt.rs @@ -79,6 +79,7 @@ pub(super) fn get_self(app: &Arc, bot_id: &str) -> anyhow::Result, bot_id: &str, args: &Value) -> any if edit.name.is_none() { anyhow::bail!("'name' is required"); } - let created = botmgmt::create_bot( + let runtime = botmgmt::requested_runtime(args)?.unwrap_or(me.runtime); + botmgmt::check_runtime_available(app, runtime)?; + let created = botmgmt::create_bot_with_runtime( app, &me.project_id, &edit, Some(&me), &bot_actor(&me, bot_id), + runtime, )?; // Bots are always-on, so there is no "created but not started" case. let mut note = "Created and started. You can send it a message now.".to_string(); @@ -160,6 +164,7 @@ pub(super) fn create_bot(app: &Arc, bot_id: &str, args: &Value) -> any "name": created.bot.name, "description": created.bot.description, "instructions": created.bot.instructions, + "runtime": created.bot.runtime, "note": note })) } @@ -193,12 +198,23 @@ pub(super) fn update_bot(app: &Arc, bot_id: &str, args: &Value) -> any // `name` addresses the target here rather than renaming it; renaming a // child would silently change an address its own children may be using. let edit = edit_from_args(args, false)?; + let runtime = botmgmt::requested_runtime(args)?.unwrap_or(target.runtime); + if runtime != target.runtime { + botmgmt::check_runtime_available(app, runtime)?; + } let updated = botmgmt::apply_identity_edit(app, &target, &edit, &bot_actor(&me, bot_id))?; + let updated = botmgmt::set_bot_runtime(app, &updated, runtime)?; + let note = if runtime != target.runtime { + "Applied. The bot's session is restarting with the selected runtime." + } else { + "Applied. The bot has been told its configuration changed." + }; Ok(json!({ "name": updated.name, "avatar": updated.avatar, "description": updated.description, - "note": "Applied. The bot has been told its configuration changed." + "runtime": updated.runtime, + "note": note })) } diff --git a/crates/gravityd/src/mcp/tools.rs b/crates/gravityd/src/mcp/tools.rs index d2eb50a..8164aa3 100644 --- a/crates/gravityd/src/mcp/tools.rs +++ b/crates/gravityd/src/mcp/tools.rs @@ -209,6 +209,7 @@ pub(super) fn list_bots(app: &Arc, bot_id: &str) -> anyhow::Result anyhow::Result<()> { let project_id = Self::str_field(req, "project_id")?; - let runtime = - super::runtime::requested_runtime(req)?.unwrap_or(self.app.cfg.default_bot_runtime); + let runtime = botmgmt::requested_runtime(req)?.unwrap_or(self.app.cfg.default_bot_runtime); // An explicit `null` is how a client with no name to give says so, so // it takes the placeholder path rather than failing as a bad string. let supplied = match req.get("name") { diff --git a/crates/gravityd/src/ws/runtime.rs b/crates/gravityd/src/ws/runtime.rs index 4527c33..270809a 100644 --- a/crates/gravityd/src/ws/runtime.rs +++ b/crates/gravityd/src/ws/runtime.rs @@ -1,14 +1,7 @@ use super::Conn; -use crate::events::Push; -use bus::BotRuntime; +use crate::botmgmt::{self, requested_runtime}; use serde_json::{json, Value}; -pub(super) fn requested_runtime(req: &Value) -> anyhow::Result> { - req.get("runtime") - .map(|value| serde_json::from_value(value.clone()).map_err(anyhow::Error::from)) - .transpose() -} - impl Conn { pub(super) fn set_bot_runtime(&self, req_id: &Value, req: &Value) -> anyhow::Result<()> { let bot_id = Self::str_field(req, "bot_id")?; @@ -23,24 +16,23 @@ impl Conn { return Ok(()); } }; - let mut bot = self + let bot = self .app .db .get_live_bot(bot_id)? .ok_or_else(|| anyhow::anyhow!("bot not found"))?; - if bot.runtime != runtime { - if let Err(error) = self.app.supervisor.adapter().check_available(runtime) { + let bot = match botmgmt::set_bot_runtime(&self.app, &bot, runtime) { + Ok(bot) => bot, + Err(error) + if error + .downcast_ref::() + .is_some() => + { self.reply_err(req_id, "runtime_unavailable", &format!("{error:#}")); return Ok(()); } - self.app.db.set_bot_runtime(bot_id, runtime)?; - if let Err(error) = self.app.supervisor.restart_bot(bot_id) { - self.app.db.set_bot_runtime(bot_id, bot.runtime)?; - return Err(error); - } - bot.runtime = runtime; - self.app.events.push(Push::BotUpdated { bot: bot.clone() }); - } + Err(error) => return Err(error), + }; self.send(json!({ "type": "bot", "req_id": req_id, "bot": self.bot_json(&bot) })); Ok(()) } diff --git a/crates/gravityd/tests/mcp_runtime.rs b/crates/gravityd/tests/mcp_runtime.rs new file mode 100644 index 0000000..35e3c8b --- /dev/null +++ b/crates/gravityd/tests/mcp_runtime.rs @@ -0,0 +1,280 @@ +//! Runtime selection through the real bot-authenticated MCP endpoint. + +mod common; + +use bus::BotRuntime; +use common::{spawn_daemon, spawn_daemon_with, McpClient, TestDaemon, WsClient}; +use gravityd::config::RuntimeKind; +use serde_json::{json, Value}; + +async fn lead(d: &TestDaemon, runtime: BotRuntime) -> (Value, McpClient) { + let mut client = WsClient::connect(d).await; + let project = client + .request(json!({"type":"create_project", "name":"Team"})) + .await; + let reply = client + .request(json!({ + "type":"create_bot", "project_id":project["project"]["id"], + "name":"Lead", "runtime":runtime + })) + .await; + assert_eq!(reply["type"], "bot", "{reply}"); + let bot = reply["bot"].clone(); + let token = d + .app + .secrets + .bot_token(bot["id"].as_str().unwrap()) + .unwrap(); + (bot, McpClient::new(d, &token)) +} + +#[tokio::test] +async fn schemas_advertise_optional_runtime_and_results_verify_it() { + let d = spawn_daemon().await; + let (_, mut bus) = lead(&d, BotRuntime::CodexCli).await; + let manifest = bus.tools().await; + for name in ["create_bot", "update_bot"] { + let tool = manifest["tools"] + .as_array() + .unwrap() + .iter() + .find(|tool| tool["name"] == name) + .unwrap(); + assert_eq!( + tool["inputSchema"]["properties"]["runtime"]["enum"], + json!(["claude_code", "codex_cli"]) + ); + assert_eq!(tool["inputSchema"]["required"], json!(["name"])); + } + assert_eq!( + bus.call("get_self", json!({})).await["runtime"], + "codex_cli" + ); + let created = bus + .call( + "create_bot", + json!({"name":"Claude", "runtime":"claude_code"}), + ) + .await; + assert_eq!(created["runtime"], "claude_code"); + let listed = bus.call("list_bots", json!({})).await; + let child = listed["bots"] + .as_array() + .unwrap() + .iter() + .find(|bot| bot["name"] == "Claude") + .unwrap(); + assert_eq!(child["runtime"], "claude_code"); + let token = d + .app + .secrets + .bot_token(child["id"].as_str().unwrap()) + .unwrap(); + let mut child_bus = McpClient::new(&d, &token); + assert_eq!( + child_bus.call("get_self", json!({})).await["runtime"], + "claude_code" + ); +} + +#[tokio::test] +async fn children_inherit_or_override_both_providers_independently_of_default() { + for (parent, other) in [ + (BotRuntime::ClaudeCode, BotRuntime::CodexCli), + (BotRuntime::CodexCli, BotRuntime::ClaudeCode), + ] { + let d = spawn_daemon_with(|cfg| cfg.default_bot_runtime = other).await; + let (lead, mut bus) = lead(&d, parent).await; + assert_eq!( + bus.call("create_bot", json!({"name":"Inherited"})).await["runtime"], + json!(parent) + ); + assert_eq!( + bus.call("create_bot", json!({"name":"Override", "runtime":other})) + .await["runtime"], + json!(other) + ); + let project_id = lead["project_id"].as_str().unwrap(); + assert_eq!( + d.app + .db + .get_bot_by_name(project_id, "Inherited") + .unwrap() + .unwrap() + .runtime, + parent + ); + assert_eq!( + d.app + .db + .get_bot_by_name(project_id, "Override") + .unwrap() + .unwrap() + .runtime, + other + ); + } +} + +#[tokio::test] +async fn child_runtime_updates_restart_and_preserve_workspace_and_omitted_choice() { + let d = spawn_daemon().await; + let (lead, mut bus) = lead(&d, BotRuntime::ClaudeCode).await; + bus.call("create_bot", json!({"name":"Helper"})).await; + let pid = lead["project_id"].as_str().unwrap(); + let before = d.app.db.get_bot_by_name(pid, "Helper").unwrap().unwrap(); + let updated = bus + .call( + "update_bot", + json!({"name":"Helper", "runtime":"codex_cli"}), + ) + .await; + assert_eq!(updated["runtime"], "codex_cli"); + assert!(updated["note"].as_str().unwrap().contains("restarting")); + tokio::time::timeout(std::time::Duration::from_secs(6), async { + loop { + if d.app.supervisor.active_total() == 1 { + break; + } + tokio::time::sleep(std::time::Duration::from_millis(20)).await; + } + }) + .await + .unwrap(); + d.app.supervisor.reconcile(); + assert_eq!(d.app.supervisor.active_total(), 2); + let after = d.app.db.get_bot(&before.id).unwrap().unwrap(); + assert_eq!(after.runtime, BotRuntime::CodexCli); + assert_eq!(after.workspace_path, before.workspace_path); + for args in [ + json!({"name":"Helper", "runtime":"codex_cli"}), + json!({"name":"Helper", "description":"changed"}), + ] { + assert_eq!(bus.call("update_bot", args).await["runtime"], "codex_cli"); + assert_eq!( + d.app.supervisor.active_total(), + 2, + "same/omitted runtime must not restart" + ); + } +} + +#[tokio::test] +async fn invalid_choices_do_not_create_bots_or_change_identity() { + let d = spawn_daemon().await; + let (lead, mut bus) = lead(&d, BotRuntime::ClaudeCode).await; + bus.call( + "create_bot", + json!({"name":"Helper", "description":"original"}), + ) + .await; + let pid = lead["project_id"].as_str().unwrap(); + for runtime in [json!("unknown"), json!(null), json!(42)] { + let error = bus + .call_raw("create_bot", json!({"name":"Invalid", "runtime":runtime})) + .await; + assert_eq!(error["isError"], true); + assert!(d.app.db.get_bot_by_name(pid, "Invalid").unwrap().is_none()); + let error = bus + .call_raw( + "update_bot", + json!({"name":"Helper", "runtime":runtime, "description":"bad"}), + ) + .await; + assert_eq!(error["isError"], true); + let child = d.app.db.get_bot_by_name(pid, "Helper").unwrap().unwrap(); + assert_eq!(child.runtime, BotRuntime::ClaudeCode); + assert_eq!(child.description, "original"); + } +} + +#[tokio::test] +async fn runtime_changes_require_direct_parentage() { + let d = spawn_daemon().await; + let (lead, mut bus) = lead(&d, BotRuntime::ClaudeCode).await; + bus.call("create_bot", json!({"name":"Child"})).await; + let pid = lead["project_id"].as_str().unwrap(); + let child = d.app.db.get_bot_by_name(pid, "Child").unwrap().unwrap(); + let token = d.app.secrets.bot_token(&child.id).unwrap(); + let mut child_bus = McpClient::new(&d, &token); + child_bus + .call("create_bot", json!({"name":"Grandchild"})) + .await; + let mut ws = WsClient::connect(&d).await; + common::create_bot(&mut ws, pid, "Peer").await; + for name in ["Lead", "Peer", "Grandchild"] { + let error = bus + .call_raw("update_bot", json!({"name":name, "runtime":"codex_cli"})) + .await; + assert_eq!(error["isError"], true, "{error}"); + assert_eq!( + d.app + .db + .get_bot_by_name(pid, name) + .unwrap() + .unwrap() + .runtime, + BotRuntime::ClaudeCode + ); + } +} + +#[tokio::test] +async fn unavailable_cli_rejects_creation_and_changes_before_mutation() { + let d = spawn_daemon_with(|cfg| { + cfg.runtime = RuntimeKind::Pty; + cfg.user_home = cfg.home.join("user"); + cfg.claude_bin = cfg.home.join("missing-claude.exe").display().to_string(); + cfg.codex_bin = cfg.home.join("missing-codex.exe").display().to_string(); + }) + .await; + let (lead, mut bus) = lead(&d, BotRuntime::ClaudeCode).await; + let pid = lead["project_id"].as_str().unwrap(); + let parent_id = lead["id"].as_str().unwrap(); + let workspace = d.app.cfg.home.join("unused-workspace"); + let child = d + .app + .db + .create_bot_with_runtime( + pid, + "Helper", + "original", + "help", + "icon:nova", + &workspace.display().to_string(), + "helper", + Some(parent_id), + BotRuntime::ClaudeCode, + ) + .unwrap(); + for runtime in ["claude_code", "codex_cli"] { + let error = bus + .call_raw( + "create_bot", + json!({"name":"Unavailable", "runtime":runtime}), + ) + .await; + assert_eq!(error["isError"], true, "{error}"); + assert!(d + .app + .db + .get_bot_by_name(pid, "Unavailable") + .unwrap() + .is_none()); + } + let error = bus + .call_raw( + "update_bot", + json!({"name":"Helper", "runtime":"codex_cli", "description":"bad"}), + ) + .await; + assert_eq!(error["isError"], true, "{error}"); + let after = d.app.db.get_bot(&child.id).unwrap().unwrap(); + assert_eq!(after.runtime, BotRuntime::ClaudeCode); + assert_eq!(after.description, "original"); + let mut ws = WsClient::connect(&d).await; + let error = ws + .request(json!({"type":"set_bot_runtime", "bot_id":child.id, "runtime":"codex_cli"})) + .await; + assert_eq!(error["code"], "runtime_unavailable", "{error}"); +} diff --git a/docs/bot-runtimes.md b/docs/bot-runtimes.md index 92a6950..163cffc 100644 --- a/docs/bot-runtimes.md +++ b/docs/bot-runtimes.md @@ -19,8 +19,16 @@ codex_args = [] ``` Use an absolute `codex_bin` or `claude_bin` path when the daemon service cannot find -the CLI on its PATH. Bot-authored children inherit their creator's runtime. Control -clients may also pass `runtime` when creating a bot. The `runtime = "double"` daemon +the CLI on its PATH. Bot-authored children inherit their creator's runtime when +the MCP `create_bot` call omits `runtime`. Both `create_bot` and `update_bot` accept +`runtime: "claude_code"` or `runtime: "codex_cli"`; `update_bot` can only change a +bot the caller created and leaves the runtime unchanged when omitted. These tools +return the saved runtime, also available through `list_bots` and `get_self`. +For example, `create_bot({"name":"Reviewer","runtime":"claude_code"})` creates a +Claude bot even when the caller uses Codex. A runtime change interrupts the child's +current turn and restarts it, just like the client picker. The selected CLI must +be available before creation or switching succeeds. Control clients may also pass +`runtime` when creating a bot. The `runtime = "double"` daemon setting continues to select the deterministic test adapter for every bot. ## Codex terminal From 3bbfb78e628841aa75f2ec913987011bb1da9d69 Mon Sep 17 00:00:00 2001 From: Manuel Rinaldi Date: Thu, 1 Oct 2026 14:50:23 +0300 Subject: [PATCH 09/55] fix(windows): install the daemon with the desktop installer --- .github/workflows/windows.yml | 2 + .../desktop/src-tauri/tauri.windows.conf.json | 3 +- .../src-tauri/windows/installer-hooks.nsh | 33 +++++ crates/gravityd/src/service/windows.rs | 15 +++ docs/bot-runtimes.md | 15 +-- docs/windows.md | 17 ++- scripts/notices.py | 1 + scripts/test-windows-installer.ps1 | 127 ++++++++++++++++++ third-party/inventory.json | 7 +- 9 files changed, 202 insertions(+), 18 deletions(-) create mode 100644 apps/desktop/src-tauri/windows/installer-hooks.nsh create mode 100644 scripts/test-windows-installer.ps1 diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 4557777..33a1542 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -58,6 +58,8 @@ jobs: cargo test --locked --manifest-path apps/desktop/src-tauri/Cargo.toml - name: Build installer run: pnpm --dir apps/desktop tauri build --bundles nsis + - name: Check installer lifecycle + run: ./scripts/test-windows-installer.ps1 -Installer (Get-ChildItem apps/desktop/src-tauri/target/release/bundle/nsis/*-setup.exe).FullName - uses: actions/upload-artifact@v4 with: name: gravity-windows-x64 diff --git a/apps/desktop/src-tauri/tauri.windows.conf.json b/apps/desktop/src-tauri/tauri.windows.conf.json index 68dc3e5..a354ad7 100644 --- a/apps/desktop/src-tauri/tauri.windows.conf.json +++ b/apps/desktop/src-tauri/tauri.windows.conf.json @@ -15,7 +15,8 @@ "targets": ["nsis"], "windows": { "nsis": { - "installMode": "currentUser" + "installMode": "currentUser", + "installerHooks": "windows/installer-hooks.nsh" } } } diff --git a/apps/desktop/src-tauri/windows/installer-hooks.nsh b/apps/desktop/src-tauri/windows/installer-hooks.nsh new file mode 100644 index 0000000..b860ec0 --- /dev/null +++ b/apps/desktop/src-tauri/windows/installer-hooks.nsh @@ -0,0 +1,33 @@ +; The desktop and its matching managed daemon are one installation. +!include "LogicLib.nsh" + +!macro GRAVITY_SERVICE ACTION + Push $0 + Push $1 + DetailPrint "Gravity background daemon: ${ACTION}" + nsExec::ExecToStack /TIMEOUT=60000 '"$INSTDIR\gravityd.exe" service ${ACTION}' + Pop $0 + Pop $1 + ${If} $0 != 0 + DetailPrint "$1" + MessageBox MB_OK|MB_ICONSTOP "Gravity could not ${ACTION} its background daemon.$\r$\n$\r$\n$1" /SD IDOK + Pop $1 + Pop $0 + SetErrorLevel 1 + Abort "Gravity background daemon ${ACTION} failed." + ${EndIf} + Pop $1 + Pop $0 +!macroend + +!macro NSIS_HOOK_POSTINSTALL + !insertmacro GRAVITY_SERVICE install +!macroend + +!macro NSIS_HOOK_PREUNINSTALL + ; Ask the user to close the desktop before stopping its background sessions. + !insertmacro CheckIfAppIsRunning "${MAINBINARYNAME}.exe" "${PRODUCTNAME}" + ${If} ${FileExists} "$INSTDIR\gravityd.exe" + !insertmacro GRAVITY_SERVICE uninstall + ${EndIf} +!macroend diff --git a/crates/gravityd/src/service/windows.rs b/crates/gravityd/src/service/windows.rs index 250f584..95c4b0b 100644 --- a/crates/gravityd/src/service/windows.rs +++ b/crates/gravityd/src/service/windows.rs @@ -183,6 +183,10 @@ pub fn reload(paths: &ServicePaths) -> anyhow::Result<()> { } pub fn uninstall(paths: &ServicePaths) -> anyhow::Result<()> { + // The app may be removed after the user already uninstalled its daemon. + if !paths.plist_path().is_file() { + return Ok(()); + } stop(paths)?; run_task(&["/delete", "/tn", &task_name(paths)?, "/f"])?; for path in [ @@ -215,6 +219,17 @@ pub fn status(paths: &ServicePaths, configured_port: u16) -> bool { mod tests { use super::*; + #[test] + fn uninstall_without_an_installed_daemon_is_a_no_op() { + let root = tempfile::tempdir().expect("temporary home"); + for home in [root.path().to_path_buf(), root.path().join("absent")] { + let paths = ServicePaths::new(home.clone(), root.path().to_path_buf()); + uninstall(&paths).expect("already uninstalled"); + assert!(!paths.plist_path().exists()); + assert!(!paths.bin_path().exists()); + } + } + #[test] fn task_is_scoped_to_current_user_and_escapes_paths() { let paths = ServicePaths::new( diff --git a/docs/bot-runtimes.md b/docs/bot-runtimes.md index 163cffc..271fa40 100644 --- a/docs/bot-runtimes.md +++ b/docs/bot-runtimes.md @@ -68,13 +68,8 @@ Codex bots. CLI authentication and a live model response require the user's acco The default tests use a local App Server fixture. Opt-in native CLI tests use a local mock Responses provider without paid model requests. -For a development installer with the same version number as the installed build, -refresh the managed daemon explicitly after installing: - -```powershell -.\gravityd.exe service install -``` - -Use the daemon built with that installer. Running its `.exe` without `service -install` starts a foreground daemon and does not replace the managed service. The -refresh restarts bot sessions while preserving projects and configuration. +The Windows installer includes and installs the matching daemon automatically. +Running it again refreshes the managed daemon even for the same app version; +there is no separate daemon download or install command. The refresh restarts bot +sessions while preserving projects and configuration. The desktop uninstaller +also removes the managed daemon task while keeping that data. diff --git a/docs/windows.md b/docs/windows.md index e048e9f..1bbc847 100644 --- a/docs/windows.md +++ b/docs/windows.md @@ -18,12 +18,18 @@ The installer is written under `apps/desktop/src-tauri/target/release/bundle/nsis/`. Tauri automatically merges `tauri.windows.conf.json`, which selects a per-user NSIS installer and native window decorations. The Windows CI workflow checks the daemon, frontend, native -shell, and installer and uploads the installer as a build artifact. It does not +shell, and installer and uploads the installer as a build artifact. The actual +installer lifecycle test checks fresh install, same-version refresh, uninstall, +and uninstall after manual daemon removal, using an isolated test home with no +real bots. It does not publish a release or subscribe source builds to an update feed. -On first launch the setup wizard installs the bundled daemon under +The Windows installer installs and starts the bundled daemon under `%USERPROFILE%\.gravity\bin\gravityd.exe` and registers a Task Scheduler task -for the current user. It runs immediately and at user sign-in, without elevation, +for the current user. No separate daemon download or command is needed. Running +the installer again refreshes the managed daemon even when the app version has +not changed, restarting bot sessions while preserving projects and configuration. +It runs immediately and at user sign-in, without elevation, and leaves bots running when the desktop closes. The task requires an interactive user session; it does not run while that user is signed out. Logs, configuration, and state live under `%USERPROFILE%\.gravity`; `GRAVITY_HOME` overrides this root. @@ -38,7 +44,10 @@ launches and remotely reachable daemons require an available configured port. & "$env:USERPROFILE/.gravity/bin/gravityd.exe" service uninstall ``` -Uninstalling the daemon task keeps its database, configuration and workspaces. +The desktop uninstaller also stops and removes its managed daemon task. +Uninstalling keeps the database, configuration and workspaces; deleting +`%USERPROFILE%\.gravity` after uninstall gives a completely fresh setup. +The first-run wizard can still install a missing bundled daemon as a recovery step. The desktop can also connect to a remote daemon using a device token. Local Claude Code bots require an installed and authenticated native Claude Code diff --git a/scripts/notices.py b/scripts/notices.py index 6c3c46d..b91154c 100644 --- a/scripts/notices.py +++ b/scripts/notices.py @@ -35,6 +35,7 @@ ".github/workflows/release.yml", "scripts/prepare-sidecar.sh", "scripts/notices.py", "third-party/README.md", "package.json", "apps/desktop/src-tauri/tauri.windows.conf.json", "scripts/prepare-sidecar.ps1", + "apps/desktop/src-tauri/windows/installer-hooks.nsh", ".github/workflows/windows.yml", ] diff --git a/scripts/test-windows-installer.ps1 b/scripts/test-windows-installer.ps1 new file mode 100644 index 0000000..d804d0c --- /dev/null +++ b/scripts/test-windows-installer.ps1 @@ -0,0 +1,127 @@ +# Exercise the actual NSIS installer without launching the desktop or real bots. +[CmdletBinding()] +param([Parameter(Mandatory = $true)][string]$Installer) +$ErrorActionPreference = 'Stop' +$repoRoot = Split-Path -Parent $PSScriptRoot +$installerPath = (Resolve-Path -LiteralPath $Installer).Path +$uninstallKey = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\Gravity' +$productKey = 'HKCU:\Software\mikolajczuk\Gravity' +if ((Test-Path -LiteralPath $uninstallKey) -or (Test-Path -LiteralPath $productKey)) { + throw 'Installer smoke test requires a clean Gravity installation registry.' +} +$testRoot = Join-Path ([IO.Path]::GetTempPath()) ('gravity-installer-smoke-' + [guid]::NewGuid()) +$installDir = Join-Path $testRoot 'app with spaces' +$stateDir = Join-Path $testRoot 'daemon state' +$daemon = Join-Path $installDir 'gravityd.exe' +$uninstaller = Join-Path $installDir 'uninstall.exe' +$previousGravityHome = $env:GRAVITY_HOME +$expectedDaemon = Join-Path $repoRoot 'apps/desktop/src-tauri/binaries/gravityd-x86_64-pc-windows-msvc.exe' +$expectedHash = (Get-FileHash -LiteralPath $expectedDaemon -Algorithm SHA256).Hash + +function Wait-For([scriptblock]$Condition, [string]$Failure) { + $deadline = [DateTime]::UtcNow.AddSeconds(45) + while (!( & $Condition )) { + if ([DateTime]::UtcNow -ge $deadline) { throw $Failure } + Start-Sleep -Milliseconds 200 + } +} + +function Install-App { + # NSIS requires /D last and unquoted, including a path containing spaces. + $process = Start-Process -FilePath $installerPath -ArgumentList @('/S', "/D=$installDir") -WindowStyle Hidden -PassThru -Wait + if ($process.ExitCode -ne 0) { throw "Installer exited $($process.ExitCode)" } + Wait-For { + $portFile = Join-Path $stateDir 'gravityd.port' + if (!(Test-Path -LiteralPath $portFile)) { return $false } + $port = (Get-Content -LiteralPath $portFile -Raw).Trim() + try { + $health = Invoke-RestMethod -Uri "http://127.0.0.1:$port/health" -TimeoutSec 2 + return $health.status -eq 'ok' + } catch { return $false } + } 'Installer did not start a healthy bundled daemon.' + foreach ($binary in @($daemon, (Join-Path $stateDir 'bin/gravityd.exe'))) { + if ((Get-FileHash -LiteralPath $binary -Algorithm SHA256).Hash -ne $expectedHash) { + throw 'Installed daemon differs from the installer sidecar.' + } + } + if (!(Test-Path -LiteralPath (Join-Path $stateDir 'gravityd-task.xml'))) { + throw 'Installer did not register the daemon task.' + } + $app = Join-Path $installDir 'gravity-desktop.exe' + if (Get-CimInstance Win32_Process | Where-Object { $_.ExecutablePath -eq $app }) { + throw 'The silent install must start the daemon without launching the desktop.' + } +} + +function Uninstall-App { + $process = Start-Process -FilePath $uninstaller -ArgumentList '/S' -WindowStyle Hidden -PassThru -Wait + if ($process.ExitCode -ne 0) { throw "Uninstaller exited $($process.ExitCode)" } + # NSIS may relaunch the uninstaller from a temporary directory. + Wait-For { + return !(Test-Path -LiteralPath $uninstallKey) -and + !(Test-Path -LiteralPath (Join-Path $installDir 'gravity-desktop.exe')) -and + !(Test-Path -LiteralPath (Join-Path $stateDir 'gravityd-task.xml')) -and + !(Test-Path -LiteralPath (Join-Path $stateDir 'bin/gravityd.exe')) + } 'Uninstaller left the app or managed daemon installed.' +} + +try { + New-Item -ItemType Directory -Path $stateDir -Force | Out-Null + $env:GRAVITY_HOME = $stateDir + $config = Join-Path $stateDir 'gravityd.toml' + $configText = "port = 49777`nruntime = `"double`"`nmax_bots_per_project = 7`n" + [IO.File]::WriteAllText($config, $configText) + $projectDir = Join-Path $stateDir 'projects/installer-sentinel' + New-Item -ItemType Directory -Path $projectDir -Force | Out-Null + $sentinel = Join-Path $projectDir 'keep.txt' + [IO.File]::WriteAllText($sentinel, 'preserved project data') + + Install-App + $firstPid = (Get-Content -LiteralPath (Join-Path $stateDir 'gravityd-task.pid') -Raw).Trim() + Write-Output 'Fresh install started the matching daemon without opening the app.' + Install-App + $secondPid = (Get-Content -LiteralPath (Join-Path $stateDir 'gravityd-task.pid') -Raw).Trim() + if ($firstPid -eq $secondPid) { throw 'Same-version reinstall did not refresh the daemon.' } + if ((Get-Content -LiteralPath $config -Raw) -ne $configText -or + (Get-Content -LiteralPath $sentinel -Raw) -ne 'preserved project data') { + throw 'Reinstall changed saved configuration or project data.' + } + Write-Output 'Same-version reinstall refreshed the daemon and preserved data.' + Uninstall-App + if (!(Test-Path -LiteralPath $config) -or !(Test-Path -LiteralPath $sentinel)) { + throw 'Uninstall removed saved configuration or project data.' + } + Write-Output 'Desktop uninstall also removed the daemon and preserved data.' + + Install-App + & $daemon service uninstall + if ($LASTEXITCODE -ne 0) { throw 'Manual daemon removal failed.' } + Uninstall-App + Write-Output 'Desktop uninstall succeeded after the daemon was already removed.' +} finally { + try { + if (Test-Path -LiteralPath $uninstaller) { Uninstall-App } + $marker = Join-Path $stateDir 'gravityd-task.xml' + if (Test-Path -LiteralPath $marker) { + & $expectedDaemon service uninstall + if ($LASTEXITCODE -ne 0) { throw 'Could not clean up the isolated daemon.' } + } + if (Test-Path -LiteralPath $productKey) { + $registeredDir = (Get-Item -LiteralPath $productKey).GetValue('') + if ($registeredDir -ne $installDir) { throw 'Another Gravity install replaced test registration.' } + Remove-Item -LiteralPath $productKey -Recurse -Force + } + if (Test-Path -LiteralPath $testRoot) { + $resolved = (Resolve-Path -LiteralPath $testRoot).Path + $temporaryRoot = [IO.Path]::GetFullPath([IO.Path]::GetTempPath()).TrimEnd('\') + if (!$resolved.StartsWith($temporaryRoot + '\', [StringComparison]::OrdinalIgnoreCase) -or + [IO.Path]::GetFileName($resolved) -notlike 'gravity-installer-smoke-*') { + throw 'Refusing cleanup outside the isolated installer test directory.' + } + if (Get-ChildItem -LiteralPath $resolved -Recurse -Force -Attributes ReparsePoint) { + throw 'Installer test directory contains unexpected links.' + } + Remove-Item -LiteralPath $resolved -Recurse -Force + } + } finally { $env:GRAVITY_HOME = $previousGravityHome } +} diff --git a/third-party/inventory.json b/third-party/inventory.json index 57fbffd..1d7143a 100644 --- a/third-party/inventory.json +++ b/third-party/inventory.json @@ -23,12 +23,13 @@ "apps/marketing/wrangler.updater-bridge.jsonc": "31c766018eef47e0b815ccfe2c69123bdb371459de3f99d566bc956780832fb8", ".github/workflows/release.yml": "08d00202ab09a5609acc8db441b73e7a5573f29de5a0884339b0f1759416de5d", "scripts/prepare-sidecar.sh": "8dc536dc0f50c0a8acf9ca6eeea72df78b801ce6a9cdff82c75d7a6c76f8429e", - "scripts/notices.py": "e38d615e2572b5fe96b088c60aabba1f4cc48261df5ecc197691e0f099bc2eb8", + "scripts/notices.py": "edfd976121b85838ea239dc1d36912e5b26d2a9d1af1127df16219ef07dfe148", "third-party/README.md": "5f7e36f4b23308dd2208414d119004d27d20095d24a28fd5b84a0fb953c50f9c", "package.json": "9298a6dafb620f1f77bba3b040a1856bbe3780978db1603eec5b8662bdf8adac", - "apps/desktop/src-tauri/tauri.windows.conf.json": "497e3dff9018b463f4920d78071145dda5eb53439bc031b203ced212a3e9c639", + "apps/desktop/src-tauri/tauri.windows.conf.json": "e887c0599387bb5350519b2c19512a0d76d7483153149c9e4cf074d2512b0b72", "scripts/prepare-sidecar.ps1": "ed7a8a164ed95523a9f20a58f8a28107d8976ee364e70e20e9541f3998edf103", - ".github/workflows/windows.yml": "8bbf2d68be3941cbf8b3f2be532f7de030b8bfb16bc53819e49c4335a0ef439a", + "apps/desktop/src-tauri/windows/installer-hooks.nsh": "d1e564c79aa356e916c05f5dd2b940f7b583910c92a1d93f0df31e046e73dcbe", + ".github/workflows/windows.yml": "0aad95c5dddf52535092389e697a837ac0b3522576791f6536e90d696fe21bc7", "third-party/overrides/0322ceec0b8f15aa2d74a6173c07cc8c6f3764b3.txt": "f5c342c49f3ac804f3e8e7bb62a8040a44c50d47bb36902b1abd13f66a1adf8b", "third-party/overrides/11069edd79019f7dafbe3138841cf289209270dd.txt": "8173d5c29b4f956d532781d2b86e4e30f83e6b7878dce18c919451d6ba707c90", "third-party/overrides/16fe87b06e802f094b3fbb0894b137bca2b16ef1.txt": "a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2", From b464e02928bf26360182db2d5e2149bb81e4e1c5 Mon Sep 17 00:00:00 2001 From: Manuel Rinaldi Date: Thu, 1 Oct 2026 15:23:08 +0300 Subject: [PATCH 10/55] fix(windows): clarify unavailable daemon ports --- .../components/settings/DaemonSettings.test.tsx | 9 +++++++-- .../src/components/settings/DaemonSettings.tsx | 10 +++++----- crates/gravityd/src/main.rs | 4 ++-- crates/gravityd/src/server/port.rs | 4 ++-- docs/images/port-fallback-warning.png | Bin 0 -> 29343 bytes docs/windows.md | 7 +++++-- 6 files changed, 21 insertions(+), 13 deletions(-) create mode 100644 docs/images/port-fallback-warning.png diff --git a/apps/desktop/src/components/settings/DaemonSettings.test.tsx b/apps/desktop/src/components/settings/DaemonSettings.test.tsx index 5a01fe0..0b2732e 100644 --- a/apps/desktop/src/components/settings/DaemonSettings.test.tsx +++ b/apps/desktop/src/components/settings/DaemonSettings.test.tsx @@ -61,7 +61,7 @@ describe("DaemonSettings", () => { expect(screen.getByLabelText("Auto-compact window")).toHaveValue("250000"); }); - it("flags a negotiated port as the compromise it is", async () => { + it("explains an unavailable port without assuming a process or platform", async () => { const daemon = new FakeDaemon().onRequest("get_config", () => ({ type: "config" as const, req_id: "1", @@ -70,7 +70,12 @@ describe("DaemonSettings", () => { renderPane(daemon); expect(await screen.findByText("50123 (not 49777)")).toBeInTheDocument(); - expect(screen.getByText(/bot bus moved with it/)).toBeInTheDocument(); + expect(screen.getByText(/Port 49777 was unavailable at startup/)).toHaveTextContent( + /Gravity is using 50123 for the app and bot bus/, + ); + expect(screen.getByText(/operating-system reservation/)).toHaveTextContent( + /choose an available port in gravityd.toml and restart the daemon/, + ); }); it("saves a new auto-compact window", async () => { diff --git a/apps/desktop/src/components/settings/DaemonSettings.tsx b/apps/desktop/src/components/settings/DaemonSettings.tsx index e35b007..816ccd5 100644 --- a/apps/desktop/src/components/settings/DaemonSettings.tsx +++ b/apps/desktop/src/components/settings/DaemonSettings.tsx @@ -180,11 +180,11 @@ function DaemonLaunchConfig({ config }: { readonly config: DaemonConfig }): Reac
Also serves the bot bus at /mcp.
{negotiated ? (
- Port {config.configured_port} was in use at startup, so the daemon took {config.port}. - The bot bus moved with it: a Mac whose Claude Code policy allowlists only the - configured /mcp URL drops the bus without saying so. Free {config.configured_port} and - the app-managed daemon restarts onto it within a minute, or set negotiate_port = false - in gravityd.toml to make a collision fail loudly instead. + Port {config.configured_port} was unavailable at startup. Gravity is using{" "} + {config.port} for the app and bot bus. Another process or an operating-system + reservation can block a port. To use a fixed port, choose an available port in + gravityd.toml and restart the daemon. Remote clients and MCP URL allowlists must use + the active port.
) : null} diff --git a/crates/gravityd/src/main.rs b/crates/gravityd/src/main.rs index e6e803a..ece2371 100644 --- a/crates/gravityd/src/main.rs +++ b/crates/gravityd/src/main.rs @@ -116,8 +116,8 @@ async fn main() -> anyhow::Result<()> { tracing::warn!( configured_port = cfg.configured_port, port = cfg.port, - "serving on a negotiated port: the bot bus is no longer at the \ - configured /mcp URL, which a policy-managed Mac silently drops" + "configured port unavailable; app and bot bus are using the active port; \ + remote clients and MCP URL allowlists must use this port" ); } let home = cfg.home.clone(); diff --git a/crates/gravityd/src/server/port.rs b/crates/gravityd/src/server/port.rs index 7b6b199..2f22aa3 100644 --- a/crates/gravityd/src/server/port.rs +++ b/crates/gravityd/src/server/port.rs @@ -119,8 +119,8 @@ async fn bind_with_grace( }; if policy == PortPolicy::Strict || !port_unavailable(&error) { return Err(anyhow::Error::new(error).context(format!( - "binding port {configured_port}: stop whatever is using it, \ - or set a different `port` in gravityd.toml" + "binding port {configured_port}: another process or an operating-system \ + reservation may block this port; choose an available `port` in gravityd.toml" ))); } diff --git a/docs/images/port-fallback-warning.png b/docs/images/port-fallback-warning.png new file mode 100644 index 0000000000000000000000000000000000000000..f997a20e11aaae6c56c2acf3f37925e6bd6be6b2 GIT binary patch literal 29343 zcmeFZXH=70+b)XBh1lo+h>A)FrMG~9^d>!Y5kfCQ zC{jZYEdfFaHQDj~zITkXzp?k&XP>dpuXBDSGY{mM&pq#Xm+QJ_B3|mMQ{QB}NkKtD zt?@#|fP&($L<)*4``7;>e_|Ef%tb*#N1>te%qSppXP(yRj$;?DATp1^_yxjbU(7-WjK4xrLmHZzLBw1%u}Z&f_J92p*1Wk zaBHM8tFUC-KMJ(<^v}$;PsIPcXg%AzMnUoY?%(d@LDc_0_#fIJ)so-Kg|o0W_s#r| zft2FQOm|FJd63Y&3g~aL)$GJ^!n;NM{i$}d_(ki9EGdB%0kUb|R5T(QYLObdw*cYm#CGz-RWUgLqZn5|WbO#j)bwB2q{)v)s8meM0x zp@NyuNhd#GFtOpb58y1XQz4g4{N-wrLfh35yeX5vp&x|%ON@I?gM1a=$QRp;wC>K? zKqu5ekjc5tDKWvJUdDf)t};%tZG7W=h!!co5+2ZKe(IXHjI|AK!1Uj8=Zq(v%2Zh$ z)hqaOUJy+Iu=Ay6WGYC(R8nDgpCcfhY>$e%h=7^7o~qNXd&>vF;Z@R$Z3Gi>(PCD+ zIegB|WH1qCm5_WH^}}fdopBOsx)9zh9L6n0V)Yf*@OEpqF|%tXNMJK%Y?bz`+QR4h zv-fL*w1~7Jrv>42gWxk?_=N@UkTBw%c6NECs_G}7vi9SjI|Ax|35g|&U{_oSAAwFiOxlRW`ERbRRt7+z$ zfEIT0`XVMPhIU(o!Y85r*o7Ic9_T`9Jr9P^@aN#UiAWVZtb(z z=omb1(rj4Zt?6F>G_2hsf4E2Ma8mCcO=HzvB`EH;@6}9fAa_in?vV_7mGqb|F!Yn$ z57PPaV z|JiZRv{m+q=RMy1=8cwvO!G?R>BvZU8>u%0h>F|gp4oxK0X2FC_- zV=XmZ1N>P)=E#OjhfDU|f>GIisj?sugBCb|Zih8uf$&!+W5U%bI$>*jH@~aoz#Z02 z$Ra>DotuFIT0((eGEf&7fYRaUtX|K4p=DA=54(EdU<5`fD$|w!ks*BR3A(+l%qIwz z(mVuL%5z^Ys>zM#@G(XZ&TBfUmupJzi{dJ?mhzR_)vAT{L;QzLnVRL*$&UHsW_SJ? zoI}}*Q3}qnMb`$f-rStOT+N%SSWvCaxlAaEeoB+QtJD~t0~I7GcAMV`cpuojl;K6x7JK_hV+Qa^trQx`5?)H)Q$Q(b^O8+DtWOxEU7G zli!(9;UQ#8n%#acMGj}k`vL#o8R!+c<))kVi*Z7P2lv;2DaHZm5u7|^D?Fp8S&+<) zt5@QclF(}Zt+d=bQkg@b-6?GDjIp1}m>*HNxX9|(SDl=V7!yO~4{HC36c!CHRTRAL z>30LqhmS&bW2Mtf=vx_WSIujjB86i&o|>b|rXv;v>h*ODhDg;@41e;93S#YY$~B)E zXgeEA;)NdEgzkojeqZ-I`&w*X`(3M12(bxI!l`klnv4(T=!vvz9o{D07upX!($R?02JLJ7{+RW zAqQMy#qrZ!zjl5pD83_S{XAr!Q$H7;F`h(o`jR{Dl$f&~t-f$)0BUQ#tiIdMo%~P+ zz2Yq3YwUL$fb1yEV|FLJah9swiH8{|k{$xh_k)s{NIxzYC%e}T0=3K9?&3`h4=mdQ zbQBN!)OFtFAM6Pe9z}YYw4=2f3QD~f@U2g$fp6nzB$V_~FX|;Mqc-vi^fq4Sf0&^4 z)<_5GwO}guRPt{(w7#IAc*3eG7H~j4Zw@2%@ z$*@s88{o@ccs1L;lme)Sf<_CnadWL}n>%SK_41wjzDeMcb2p>2pRNkxyE7LCc1L>I zS!ZFB)RH$vmuknmhDM;Pt_$Xgio8b=j*^$v*cyV3O`1HCxWOj+TEZ>N~1@6rt;}!G{raG2?H7upr~a3009&jH+^^jA8!QW z3U%Vz_A8T=47CE>vgM-!L9nDQmMb;u08A;PbDtH_tp?yCi$u4VS#xlP+!ujr^+k=> z!L9->C8;Wh`?Fj4fx4>`r*Zz-`Kw+3_(G#h6i@qcXg+Y@;aW0wMDerkJwe!{%WP~zrL14<#(1y42-(XbA5MeX&cIujZ`LhGIF?6Rg}H*I_%OoD zYTsvlR?$%16lhR*))#*3oyGC8@3D(0{qkCbe zyNOUQ^k924J8Y`EUlNF=O={ekH)OPbK59owxO9XDWe+uftQJ3L=;LWWUx_;Q)(mzoI7F{W*ujEEf|vmf>tytzAKQES%N-KDBgTwfW1cWxl-F zJYQk*9c;B`ci2>tDH>(6yE48M%t*xORhXY(sq)$s;b1s}8O|-DQ=UUklY4rIW5*hF z3p3{+WU+E7wl+qB$oST+^tZ-AnUgk3Nh(r!hy3sqq~qH9>D(sDv0UF&U0@VO*%qUv zZFti|@a0<}r~)u*ncgiMPTV+7I`y$^^MBLt*nY-SE>E0g^|#JSt0oN5{6+CrL(q19 z{hsuN{d(Kp>)itN>~$DG{>*0_lPw=V039}X6`hep>H;j0v?X*y>JMVfFJOdgff0q> z7@?ePf5mwx7Nyx@4&jR%>W4A2CrXAD!K#&t0{PJ0;_UYA^Wmn#V6JOT)WIyR$9HpQ znLmmPxew?D$>$53i)81bu@LV4$(mz#fV|%@v#!!{{)kc}hGbcAp?(7cV@BNERG2Lg z{*m}Q0G-nRYPTJajNCnrPel9j*0(h9;Ra~V&$^BWP{n?$+qcObQc*bNZhHvNC`;1u zBoT$rQMlMGR}KmWacOuN1p(Hw6X?r$~)2a1&;ljA^nukK%dO0NEiw{XWpMXhe+k2DBcrAfRnIJPY z*T6$+9%(&c<>f4L&tgmY+1Y;5xwebubjWD8ihi_<(fS*R)RJjQ#6(S&J;r4DH3d)i zTrsw240nt7%W5xGl8YMFZRD=0FJ4&`NT2ZbGdZ??eQehN3z$@EWkUMzzpt0!Dwy>` z?iB>Kxopl~SjE<^E|tfXMf981os;%%1C!C*_o#N{#t2y(<-9T_HOl?EuBYvDufHdd z>xuYC?Y(+NhgHRyZcqooV*DK_4E-IIv=f$cfmhhNoPyJS(>>K+Go;Eeyl4nyYL;mW zCiW#aGfI)RkS^%-21!!D14skWb_kl04JoyAK|Z26s0Ke{D%>a^ip#e?2G3{0b`3{@ zk0%aYxIT)%euBL`%!4>ewc`hO6H?qmT4NM1mT*#jGl4Vp$&cf+0=lQ58qvoIgpF5g zsA)8!SIgW8KhDys8G5oPC+7&AXg*>YNRbJfpF#&k<_`M?G!8%=x39$R!-Z4a3tGcE zw|W+W!(&^CH9PKS0ndb;Vd{l6l%HU|lqb_rPnL!@yxGiIp`c<*?G((eEg1vb+DNi2 z>?M_4EaPg>*d@-`c|OF{<>M<9KX`l-$bEK`eqXzY+{ut}ej|GzIxOdv;=i$g`{ep4 z>c_S|xd(iFt>Yd!mfC_hz~1Cfg`N-mYe?7ff4TNQA3RK8cpBjpnip=%T&jE)Ay`Ud zXj%@|#++GA?Vs|Pj-4cgk6H0b3^S^5rduJ{N`t(bwg-Bb@$nHRDN+bDkBdaeJtDIa z;U7~d<~O0jmtp!=B4MJmdEa=q!Rq^Sb^SFFG0s~{kEPy=w_i!R^9{xheC+Q3L0rsK zkqP0N8C0>fxoiP?X>*ic5#L|3K%X)|_5A8usc{LeirR+Z>IC;Ff;J;WqC{q6Z`#hr z6XIap49|cbg%I@DftH#KQ(aCbxW=FjBmuRF| z1lt+&0PgxuKnq`Q)v3g=U|ZHAw}~Kse6!PER;6ntWxfJ*!^H*FS(Vq5=$`|*Z&uy% z>YGh7`Z3i-uUP%l{H%`mKnZibP?5fog4cd$Uh_|q z2JRUbTPe|h*|892`5PRV6%B`t)d1``NlCh|UUDZ4Y<^u)-gmZnb9RLnniuA8>TXwF z`-;XiZ%VPfx$R3B-t=JO2`^eeP4d#k$om(=5b|n2GO!X?`fmf>{o)MCSU&h-WTmS) z3>Ncy<0aSWit+2tYO#;IJ2Y?});^-SxszK5IhbjTdyeDtR9kr`Y9Pug%dfWMY@}Y4 z49(4+HhUs+d-?$G7w#4lQ`S?^dkJF zt6kSxIv9vb+ZHmZF=#o1v_y@u*U{7sne3m=`Guvx#4_AMPSLW5R4mso+#g|I|woZB)L6aGsS26kA#eLCf zbi&0VUs+-o0Ar}zQhQPn+ay(@H8u1>sKY1JMaUeN2GH=F5Q|`%nvC9@Hkes@~9A8x)RE;${0%5cEY~JJ^=6|If~jjNZPCqNeWdW*p;bxXd@?q4ihRgFf`T zJK5UZm1+`UPd>`%C5$FU)lXIpK8P5*#o@1OhMowZboZ3{xz>KKBS`HA)!PHrEx~=7 z!Z~SA*{*5yTCm|x$5^0Z4!>h2R4+KM&0!QOS5Ii{M{kz;v3mw>uJ!CkF>tJ!%b z#KJc$-(L@co4GwA^+cANZyVfhWLxQl<+^Qnm$tTTfv$^IXIdF?PgQV-6=Xk=^5HFH z8eAf>MW0hXwAlQBd|@C_W+Yq5^ghtJ>WACdvsnLQxWa~AIx%8ERjsFQV8`$oFf2xcW%VJiPNvpKpH6o zD{Su8lq5jqg|lCtotmXr9kcJgzifrXUR)!X&o33W%5r7~JN@0Zm)Nj7n*4wm{OkSZ zUZ(Q`9AD9yV;Wg`b0WXCkDWy>f>-Cvn>CtaBSi zBCQ$=>cm?OEq|dK-+LW-nYG@fjh>Te(yUl2eS_coA-{WLCBO8fUR?mMoIYTGa=3n!2@}|;SYWel zEZEMSe>ag9W+O1>bNBByHjSO=Lrs$IbK)ZAP%{20i~};#-8gg1WqL=-$!B2frl;3+Lp3q@H>0haJ0EP_G1jIHm4dC@ zM6khrAq0*xU)gf16hzn_?6lmXqya5_)Fv>)KXvO?xx6WqyPP(V>9<|(S`d-^d(>jh zjXC415+&=fU^nBaL<);mywEQ=cM->JANZ2a%groAv-Qev5a;vY$Hk5!HJC&4gQ{pf z1vE}>3cByjjPD~W%4#oG{~3sT7#8=g?JiD3?pdCHSrmeVHN(TJWc=|=(0FIG9sZZP zqLQTaK%#irz)-94c2PB7k8qZ?L12@UqRw^H*=lvtp@bLEcIUmyw6}EEYLkut=WAa9 zYn-O(17Q!>=HYJJ=n+4*pmJmOS7Kgn^PPbEM%@x(aHZ_8&WT8x);W(ioVNLY(OT0U z%l?`$xYxB>8QGS~Wa{tp;mzC)Wv+-0U7DqpBKEw-ie=>_!f}Ah1h%w$fj&WYg#9$6 zv0@<5`5B_&cc=V3rCk5d(;J~Ul{-zdRzb+Vl=~p4WZV2_=1${8e$J+yIGwKtp&SF& z_7^3v?p|-Fy+g?hSY80#<3WS8$1^*cXItw}M7y`&NwmEeT3xZ+WH*WRjyLg7 zycdLlm-Tz^<-`x?3D=!>IdL<{nWVQG`miM^wH+VmW6?_M0%0*aQW4hXhruA$=XbQj zMvVM+Z#Y9kq=oW3Eo&>Q{LH~yJ<#Ii+*J337mZN@Jh=EHFTJVvGQsb0o7|qekvq@0 z{ZH0z7=Az7k;C;3Xe+P|2%qRF-%)E2IOQ4ZDKD+K)vx*ZrR20|{%))FCWt0ullgYl zNVFN>+{+pQ;Stkns_w83R|Ji>d*Mj}=6*TgMwOcFz$1r(`1UL6qWx6hR$;^B^60O@ zbl7j7k&@QrD3&@bf2ip>E;F@)O8$(=?EbhvqEZ%41Jo{CF_~GMXRTBSTYX*S{a8Ir zw_82~VS{mp6MOWUw2&EwXkVmg)~Z(0)CPlD7C8LtdQHwV@t^U}##+Z{f4V#3<<@Cazs}UP|Kw zD`YTl5r>YyPRZD@anrS+1E=qXz2<|3El3+cc64yljnlE;jsMK4-Av4RH+ZXCy`bhk zknqFe$Pd@1MF+LAs{eFWO-EiQ>kSBHtHhCgX43w9Yb=lH+1;c{L_=eOx=9w0^#b$> z_6fdxgCqAAw+Y_(x=D6_u63)3gY|nGi>;g7B~i_ma1lk!dK55qkM&!Jb@Mx^{HF-j z;W!T+g=OY(tT^{%M8)kU5$Vxr?~3*(!CKTqRt;+X?DARENEvx4fE4IL$x8oBa8(Mf zsU-qDHgF@oMxBi^%l-C>+5TDTo(b7V5);nxR85rZ>eGX4O9ik@x%np!7BOhusQrB# zbXLXAVX+TYEOaer_;#*g5(%ni+)73o7jn{YnQ{yZ#foRK*GgxBz8?SJwtKoGs50v) zvZoz6n~VR$;WQGXC8ho>3O-w>gs*J}tRN*Lh7sVtG^dWk|{Q(n)ixVRh== z7nk_go?#CU1ztSK41Yd~!?ftXFgbQ?>uK+h08USz)R2t259FUHZ-z;QFs-xm-)4W7 zA`|H{$HLYmzch$KY+N6HZ5z@P^rw0G>$)>TJMLPA@cut1JOT5cJvb_cC19b*ruT>G z>vBEkSKRWcTiR3>LXJuy3WKkW_T=2}ra4pHr?A3kyF;{jJWZd^IkTBMGr+ea@rVuy*565dzvOTYeTMNtBHtx0u9fFzL-f$6rgAU`iYGFVco_&omE$c;# zNQN3Pl_!@$UH^MZ*j|IKI=$(~<5uPe(JoSpU*P>C_MPc9u}ua{)h{M&Y&zeP`9T(?ablC%*c#bYQYOr(^LDM3tR=%C;pSzaGHbv zB-mB#8|!2nO0!{k%hK0ZfOXt$GF_Wi^UYz_oEJVZ4J>U<^CrT=S7WYDb*vynOh+x3 z`El2=F)fuf^VXhs(LpXMR<0IOIW<%y@g`JJ!G zFAae!RR9bV)&wGsb@4{!52`t+v_1*r|2b%|QS4wHGmR>n2*xkb$1;_b7Xwi16zd7Q zFRwgJBi2sC5{i;We!A!avqoN4w{+ZYwiUFNU6rqSI+1g*`AIjZ%qr^ky7)SMU}9}g zEVcZv>|j%ssQR|Az7BJ0OFWsA>wS;b)@Hxyh;p8Nz|G)9b-q+s^gko;_@_2xxjp*w zLEorAv{`)grNcEZt$|0Z#W6x{PY(>gOqfZ%8G%HO=zGPz;b}NFEUA5KZ{Bz4QKqgD z9G{(UtRy z5l8+`@RO#e6Pmiqpa10_>uh`7k9u+cWKn$KJ7HfdxeC#Cw-XjNV=t?So^`6dQ7F)I zXXP~mKp0qEie*H^%>96RF!8Ndi4p^aN zmGIEFp=yg_QI`IcYVv2_wl?t^&|+ulHzjX1=E=uFKlu{vS_>CN3jry@Z;det6~t*0 z^Q@F;5jBB!ZBoSI|wf1trE}aHqb2}nz<~^^)^{VB$cY0n){Fv`-;f0dCWr1h0 zLCF=m3hTR_QbKJSH0!_Aw0kOM#hZ&K5YSEtm+yC-H|cY-gKobpTqZt}dSxlJqk9`# zI{!RD4QQuNPj6VrBwdc%L5_xxv0R_cH&O%P`w+L_Y-sE6KXL8mORlGDpG0)3WwT## zmu`MWUUii@VQ_t=tdy(zl2$)K>olE7n(9-#$+x%(bd;N&j zO9oCtgAjblXEK#0?dT}M+m{`QQ0B3CQW2ohVZOhSJjSRfUlm#1_4_zojhB*cmr3hZ zU~@*OO{b^0QA((ck7Kp7wKFS08-IS>)AG5+==_{6IejF1g{~hk zw|_J~W7_8-XB9K*|Kula;>hBi|B_?=yM*(9bMQq^8}Cadq+$Cpmf$oml#Ud=l^pC0 zl}$Y95dspI2qfG#9|c9K%EPqO)pg4@=6fl76EK@o0*r8pNg^#SKJ&otAyHEW7pGJ0 z;O0|T!?Qp@@KFzm%p9iP0mnu*B|YQ;pFIX+tl2g$w@Q^5tf&H%FM3i)A*k%jG~vJ* zF5)Ev8GZalSG73Qx)w~zpH}^fRH>G}d>}Ba$V%=r$4=pxmDK3Hd=*t4`Sjn?)R_a13FvhBsv z!jq+_#?gajybccd_Ac6arYvI>+Q>>t*al;#mbbW!SLN!jZHP_@FQ-?%WKF?^NN%+# z%^ya!&*9;Fe@fE%tnsq<08wh$7Gdm$8L4MIZnajJ5k0^ageK|&JW**xj8@_#>4rj< zO!qr7?ee5i`%qyQVI(^TW}IKw7O5W48IsT$%8eLA`-Kdn+d8rdc#@)0vzSas^i?uX z|H>=v*P^wj6fd)c$#}>^Vj{8LYAPiw_2R2vFd70N`A3tDOYtZz;?@Eeg6msPMtg#> zLvZ*Gr03qPO~$74$`QZT9FF13tqZg$4f+15_K)}jyjMfAB9Vs9-R`JUm?#Q~kwNq9 zOV4T&(Nc^^o&Ep#L+wYa@9LV19bWI-s3-Zo5+I!)$t@qW;;1aou!jrkqyygm^+zWw z{U{I`mPs>aeK;m($4=%rDG~;B>WfkQ3~wF{DNI3U4olN}1j>ud z!`R^)Aw}(e{}0*oz_#N&4%ULN6M*S@STE~z-U}xi&LG@ukBFyjC!I}!{yUeW+=UU2 z&=++Azy>6pm198O-oHI>j-AO@NVRuu063FdsH%E-3KM91h$uKwBqSDD7OLrG;4uxy zM>Z!Y*d@z(Wd0N8onX^ndBV z|4(EJbI<9;#GOUR4)Bi$rb*9`=RPn0AIZai*0BF`^#1?sm2QOqmU*>_N>T!Lfu)lb z!uw?hQhqo!^i&gHIUp!6KM_iw_J>nT*>E>vkR5G$n1==Rgw!P$mW1%| z*!lr5-=Irrm2}1tYYSaTaqVjLU3Lm8!1`kDjN$TS z@Um@hkBG)W;bd0*#06-*bVR`oc3TQ2dYhbKXfo@r!rm0BI zBw|wvZ5zJRS?Zh|!Cz&nBG#-EHC1)`uiCoTozoDj`=y^7Dq;ORRnRqo4#5` zrLTq@?=-CSUHWVt%_!HNS_V&Dh|C))L0%UkXq=`|sjc?Z)49pzg#-G|8TFj`ts`D} z=b`R^+>>C=2ZwVZr=#X%k(q-^4Krq<9@PO7HN5v$ty4_BwB*trirp zNuBGTo0V8VrxR0!#S!DKZVy}s<9R#)wpVXA zFq{1u+mWrrM{0xc5h7uW#Z9{`yTZ9DD@N}0ShD6zU~A_*$+ZOSUJzaQAI6mqhzeR@ z+Qm7~Hv8$`=~=I(=i5JmI3hh%Ou8?;Yv{8OF`OsJsOEFSNd@9^M|HF6_qM}Rb-E(;Jdjb8>gopDU#P zsZv5L_M)S|YjREuRV+zb-|EFLRQatZ?u_aEzy$Ip&_*p+S561U3?y_ z0CZpza-UE|Wdm^|A_t+-t1;=^Qr}>elBQjV((^(KhguL1!nO z7k60mX2pwXKAc(P&rL;cJ>=h6R_z+n*KZ(;vEEY3Oks#;wEnY7L3!r@X4qtl>Fo=f zXJ<+wvIN*vQ*!XiU~OiQE;M-kWzaTB@w@4Tx>zD(@+a@&BRjpbYjc|yF5v9W4A31oclxO4euT3hkz}>vB+pn0ZA;a-xLutP z^X+tr1@Q1shI-|do|3kJazyz-0m3ho?)T6`w0q*8d!+>E#b&gGIsbe>mi+zTsnuOp^Q&rs#>uJ}5F2uv8x^{Y#4n|4tYbVCxa(52 zZZasv0>B;z@SR&vm;QCX>ZM)|%f*Uf*)ov5ca0^MNovBsO+v5Qb9LRuA8f;jOpBdF zsTyeS_UxbH{ivi+0+R0@vHy`7Y70X8*c%6YY2QV?Y1m<%L=IhrS1>q37n68EVvxpC zE5)4Pq>S11x`^ME_jJ}t5WU_Wxl>Y##%hfD<3|rjj4s<&V3ge2myg+z5C--Q38zNE3OhYvGq0z~Oa+?>6_n;mcb9l{$ujdV*ZeFR6PfH=NsYbLkBPgu@E zS28<(Rf*}A7vsE{{65e8?K&{=6xhMrDk-^XRp15m9P7*sak1HluM>V2TmaxRUIFILW4<+)JMEmpfI9@JIR?{xo0uynV!} zfQuDv7;7lSh!)SXf>bYC?8u1F;%o157KC@_xXst6+75uBCW) z_&tx>rk0M&H#S~EuvGtQQMG<4sB4P_h50d3tCg%xfb?-@scg9tW>~m9aMOz~=%N;X zX3pa1+NW6tmYuhqKx*Esaj?P5^=mzF`GT5M;!v~n=z_WVgitcl)o4CJ`N&~wVEcUa zwUR!at0SMqmY4U9geI4XBLmleEQMRD|8Ub)$}LntOBvfo!TL$v3Vla>JCvt0Y^X^J z{YtQ6i&djz8xPWW;9JFe+0$Qsn}v2b!MbZ~Em6+R%Bxow-Af&S2pqWGbc7*6zaz^e^ZI-3}yhVN|&g3h9Nr4xiRU-30k5?Ix_RRRDkFG!pRM^ zA7%Gv;F_e(jCL%y;%+uw0zdr>mqZfq=F4(3D+jMH4OJ!x;@=dnlrlk0(5?+kUs=@xzv(EPyY=xnw@^N_unQgi*66Nn+ zukU-6#upizE>W${{m%S5Wqm7WX1-*Sf+Qf9)Z5PuSSdHGbmIUa@gwb6WFg~?J&Dj@nQk#KMcqYEy z!wB6WpIz|yr{E`h{&YX6IyF_=ai4vETLH|YFV^6r!`xq!1uEU@ zV0g}*KG-n{(I%m@WyJRYl6_JpDZ(T!J8Q2v;NPE^@nz=}0Z3}<=byIEX(@}IXp!A? zHV`5x^XTVG)xYo)0$^5rSA{4nzW*-!lz0>`pnJzmAfhLsO15wo*7M9rOOJ(MaOgLc zO=qmLeU zUHmc0YE!lJ3;QHTO4eNm+#Y2o+ZRoK70{RHGFM$K1< zw<(@c0@@3^?oYS<*>KB2F%^y z8BNSP0)h|V7|TC9_TFdo%2(tRwSp$eue3Ti=swZYHDLa|w2WWKhMGOsh`jg_Pu%2@ zKv-}y;RC<0X}AI(ooX~~TDD<@b3p54q*DIeoE_h$Ah;ksdCsYe?HBW4?N7)=2|Vdv zI-X*V;T$6?F9(ki6>UM)=NmELCXsI2J>;4}GvI=Y(j#bQ!T*^>Hh{P)bI!H-q}~20 z^aCfkR#L$q6;)t^QTt^ZY3#vvR;8jL-l_h9_sEQu@On@yykotf)^<$_-am(T03eTr z=GTKYK^{}!MG=32y&vPo@}gZ*8Sq=j;a-BYWvuaSo?OmZrQ#!N>({ubllRP6K;a-I z4>98g>HMj~)xzQBfR!(N@0%IL%NM zEDiL?q1{n2;Zx)Bw|-b3rV7OzHKJq33C+*%YzRatxTxQ(* z4dWGx-z9BXYfB^fA_&oN(NOcWH|Ox5I^O+Ih%pD+JMj~!@t8N46XPS+x*M7p^~9O}-E7dQTe z8g58Z{a1M8|NX;s5oUWf8azQZ2^s0g_Q-~N0-f~4)#mJ=lT(El6=24-03P3kR%)P?H4j`ada-k!!^Url{gey&vw`jJaG>CDKae1512Vgw1OpLEFjd*;y0KFs(l8!*UoyUN9B_LPIW`oWbh_ z*`)Wfgn`RB7uX6=2&VQg<0Agv<+?DQDRUWL9nA=IOQPX+DERD zTpQ=LV9}ZLo{E$7taVy1*L&7vCASj$*1j)}46ssM7x_8Au39|DwJ^xl0vt;Q?vPIB zcj;t8yw7q^>M?XA%vi1E%2gO~NsR0;KloLp19oxWS^@I&2TYKRSOz$jr(a~Z(I0GE zPMSU8y~r-c0b3m#lNml= z=j*-!03J`x5?=BaD7a3w3rX9Q=n29uF!BuXAw-ZOy-5mxPTxJ=W{zaX z#bGo2`6jaW;?*ra0|q+V);Kri^VI`&mGm7ZUV~3Pw}07ut)-cGFk=o{te8(lUQ8RK zHN5gYuM(c@!MK(@@60DG0ZpOTn5uyfFO)B6#)E534 zA*1E~QB7BNnCaK53bqy=cmMFQnTX`3xu(dqGZ|L7?55)g21)k+5IydBX}#H55p!~^ zsCZttlVJ|*W*WFRWkYv&%o<2`V-H~!u>fzF+dV>0e4b80?R#DGO&LNFT15CoW%5eX zHpj9a%@d}}`GVAaW;wm5_Htl%Rne%U1;_HyM;W}ks8M$cc_ z#5OU;n7QqCf`k18j`(4OA;OS8^+FT&C_0kudWS*CLERlz`t={OT;92 zT=`OLi_*QC;IaQG^mc9un^BfDL73)R4C-=|S66e{8vQujKT?8eR=d;2;n9Y-KASuV zlX_<3m0yEsv9K9%cxmz=#WW{bsi^cH;;lPvY;+Qxbw(S4#cm?c(Q*_&BkFRmN8m133Cb>c0H-tS$yOSd)E#~QKS}?qOqeM76V@(5p%hG=Cj-! zpJfhLr~1|>qbCx-WQK@IhBYPrR+56J5){=28rJ6x*arhHR~14=3^K#I0)pLWgWu`?W$l0Vyf!cJ*aN zm-te|Xt!{SFQvCiP-1gb>oI%$QBE#d#9+DmnR!sAhvnL3s|KY^NAhq-hz)S1WsZqD zrj@<(eay&G<&3#Z91SC(Dl8_dDLSL2BV(BLz-^h;3zxXAdk)6`J{NhheSmDR{pzfT zcmnvO0vQinEZA#FwGs8m@`@1~pEMPE$?=opssfY;M>sfQq+}uHx#aGuSW~`h9#XA* zTYcYI)!$n##P=G7hYlG~9)hk*5EZ@OjSwLVj2dY&OzS0THXmQckIJlD`*fxUkDPN--xD|itU9|A00bS$_s`3&2Q#yV1 zTe<2Aywox3xaL7alDQNvS{7-_;GqRJw}47O?3Ff0DJ33$bK5ZWud^uc!NgD=GV`k zKWDdEYGR+fIxVt6+!VlxGwk z{LZ=4*607L<47RBQr%eZVelcR(XioFZcd5^vEGW2xhxK&yldjwg^?y`eZQxHt|%JC zg)J{G!MO=hC@@SGrugdS>D$eFUqQYzGKT7?!`LgAPRWsT+GWm^I&>AFo-H}>!OM*S zz)A!l^#y!Lbv!NPt&m;ViMTJ)i>-Aw9dTFgC&27ASMm9T@x2x8T)P#YclDWo%@#i_ zeO1IY>C6Vy^QQ6gSijd6n#YoBF2<}NiMUCvs;j7^Uu{YrnSS4#;tV-|-%T(_fP7dG zuW{lDx0jzD?gbqN^z{5zbSzp~zHt6#k(uyq+fE!xVWf)K{e`-@4icGbT96$+(VucFNyhbFs@- zaMZ6Q_}jO`seX%qTYys~i~r2p0(G-4?M>57Y8pSPC>P#cGu(N)b?c(h1AGYTTzQ&3 z!)CYBBn@KMncZWsM$PH{l3ZYUGrq+YQC~r!CD}=B<`y>g1pHPh z^k|E{OJ2|6sN}LC_eqxZ`G9HRU03a(ckULY-!|_{_yynxy>H%XESc~fWwz1gmDQCz z;-D9AH|rG#77bf9zg#e*a#qqqvKSNGg2M}CB^s1XO*`tv(8!lls@4GF%b*#X+UPJ| z%I3-kKbf!x@$bBXJ|<%xvlS^MsGR#1L4R7&23Ia+#SFNYoj%3i&arHvs=TN@6 zx`=Caq- zXwFiFEICE7H@|ES-ESBb9c8)Ih-Zlu9#-%As1?k9h;Lx8891lnpRC}dFJQ3Mz%@7v zNv6b!Ef*i(BP1B!+nvJQw)eY8=HRW$DE`QDep>>&=bk9Nz*}p5wgnYgAr{MtbP?_5 zc%Sd`&CFi0rMI>GP~n1V664f#8Tz%@daX*9#nEtOIqZFsilx^Im}@8aFIh6W?OR*l z+!p;f+`s2v$9y6KXx2P$ET{?u8<-)AcdZp)sElx)`cEI1{rvIFRWBX4n^Jl%l@Ow( z*u=c<#gf?o_P8%a-`^PV`DaitRko+o*2^g=ErQk*4VxooSSr zp7q;Hmz5Mj2cFSblNn1;`-WV(S;rK!a_Q%J`i+k!ZT-uYvA-&~V<*0C|Fs6Pm~H>= zW4d@JVQ2+Ka}=*?YHFDw`yRa3C{WxljC|WN2fQn_XoQz# zvTG$tP-p&AlVO09nW=Ra8QJT7c|vF(&OWaqM=F&LMCbIE;IJ_`%3nn@2aQ+#C%a8& zqz?Z5BzE()(qM;N4q}w?M9y!cG_%-9EzPn)z%+o#Jow1(!}$-@qsU(m3A7nWeo;ak zE9M`40(}(rT5$iZyt9gGt83eJTS{r6K#MysUW&97FBB8^*kF$22Np`COxI>z9=Y3 zco}O{>ow>9Y4-hh;^&g3{z9FKC5xi63%zgmMZt}dVYhj17huF(9+hTpnGNdvcn6QE z<+rZ3_IB&^&wZ(ai#f<)%uc^toHaUkCB%)J8TZLu1ggpSx6VOzH(0Bo2&RIp)R$$a-YDU{K6z#hirWh{i6Wr=HhE(vA_ihqpf)4WNcX7iu)phC)IcB^FfHa&50xtovtO zfBW@(4%Huti}Y(TJbRJl{A9SMF#5U3^n73!rJ0DNCh z=;T-(r=loVf}`4JvOMg7&t8$d$6D*^2W6LB(Tbek2mmrsBzLWP<1OwTWxU=0X+Cet zKffyjB311#QE<58w#W2sssH`#LfVld^-#b&JKa;c$k52^o_?ukp>(rHpM_U0wWFu2C@Gf65Fb2_B0kRRdR5sqrXew5D-IsBHb*1kPW)(+rjWL1~Ga?1GlO@GH?BKUOc0e){(J^};czlhk#-UB#vWC5IB0&o}B1h4X%u8eR z3CzULX;HJ4U>i0P#XJ0AZl>d5LlDZMAQ;0Vyd)3h_h+k`6v!_?350!T)@N2Z{Y%zK zce-ODDRiD0%lWMOU|n{YFWGnoU&D=OBfK{&F`0O&mU}odDzznB>*0Y8*-hp-UtNHK z7T(mc#IHC4fS@;T9!=9PM1hYPU9CJdi(O^b#FpH%tJo?Ga{`9PbdR9fk#g(&`KMi| zvprrf>2*5SjVNKE<5fG5Pde>_m*{yOG0Ncm{jrD~<=5Ebb|m~8lvNM(y|%zL2`TQ7 zh9Ak*J@)MkPEG9UTKEQbQgcq^ZYS3VGmZr_mv0MYKu$7{TEqUXv;<{Yuo(tYev+xo zN&_i=pmV0pL&lBQu}bv)9nz#EqKuhuRj)Q0wBY}wJG*^79SH9&(|_&0|5SLD^jjFv z&xIXv@dHT{y5l32E`Sy`0AJ){W-=m3tXAS$=exz10F|uIBCSh$DD8eB_;A-DG&1%P zd`nI$0K~LP`v({$kl(PM{D>bXM?RQ;wC&x-@S}QAaCDyu>X&+p?y2fb4e(tPnCiOL zv$OWmxpQ5ro&OTJAO?v~9ZJvC*+-{(9qSCIbk<0Smp+=q8mP=tg3xWy+oF*m&X^)J zh{i<(OeCLH9w}RVCwm#t^4=0xT2M8u+WuWWvD@w@p%6ne0Qwz=A_dNPH6s)X`~J(k zDOYlLkLlX9b`oO05II#n=jMSbs%*1wLXGz8W$AJB5RZe_V{4DN3x{k88M5pghO_~T ziIC(UXwqdc-<+O`6>%{voTZx9D()_hx-$%+*PG1I4TkN{PfjEH+}R3;)fSZ!_u>mx-#sCL{VTEgaTdAq4M{HFDR30=gYam8(G;BZh&e^)wKwcU|0=Rs z%kGr^VfHed`i)yYCYyJ~qTz=uyPQNdzxJ0dh?egSor+p5@QvgtHgRWWtb!B)2V0kv zs5c(x0|yMlK^;y?_f$F6t4c`tCiGNxfoze`28!ddv6eAMr}|vuJP($R7hjU6&t&Yl zyzQ$>u6p2RmolxW`pk&y6@%Bg(>-5c;mU~kM`oZ=%+MhuP)+Eq;m#32E4j^=_T;p~27>Q^-#|PZ_Qv5kEUS?1m#2ABBMXsNhL}mlT@VAZkp0czx&b7^@!|73 z(|)6gpnZ&N?CwTH2%K6!NUxqZjnlw4Fk1mi_io>zq>^HiQ$K&pW4aMqN%ZlcB?cAk z!O2&yemzD)AyS1@Vc#u;E7BuXQ@?A(NEE3R7%Vg>KkE!0G766gD`xJ1@Q}CDNZ7y!db&=kk|jX#hhK5Ch-u z2Tq;4R1koHawH~e!uK7{AcTgI>uS3r-V#-ph3>*nVd<&kR_Fl^!aFNL!??7-)sFOiZvn zuYBs=I4;Y{sjnaHJV&0EoHg*<*{?E1j+bU6J@!zk>p|&9w;W4ayyIg3rpG=Od!L-| zDUD5?N+VQYVM&|{4r?@Ol8$#>|J@7FoiC#TT^|VM9N4}OSJyYCfc7DP0rA)4jc1s8 zIp+<3w^Z9(v~+8@{7K{AxxyO16D!H2y(C@r(nR(QNQ?k?0-~AcHe57*CZvNF{VIR? zpUw$BPFpm%vaElfWb^w|$Fm5k$Btjx01s=WeO7l60X_`P1MT3+E{U{B+?u8* zhd$%@6Y?DR&*lg{RTKY91AYG&j^JEOTd@F(gYO_Pi~U5@@t=@#E_7Cu(hn2==MLlGA~G{oO8SmNYsf;#cwM~Yb|0daKD(stAl&gwX z4czAg6v?MeXGAo8bFaj(ak02R)bAobf65xvHRyWPd{9klW7I&#PF}%>UkQ1T19ePq z<;1Py#B>acYJv&R(re^cTT8HT^9`|@vN!ShH=La&(AkuF=$T#aTG`iG+DEK%H!dad zSz0(AYbu9!-fm$v6%8a_7QU&X_PO0sG5VDgWXH6)TQR_UiupqyqqWQyTHut%SMH%> zWI0CaZsTJ&0`Zihcjb+3&4yOyPNRsscCAww{!CG8GKRYjz|{khP6f zUVDC8BASAuv43MT?tq;sV_0@J>ie^fK=L%ehk0i>g~SYX@ggcT=}JRDXA*;4zGJ|? zJOk=uH8JdOW#w;GK>l$DZZ3n3uM8dz|B&nA6}_kkLftA*aDTjh?N@nC)Oz3{sNC!1-m& zAxs$JermTj0v$R!J?wT?!s9xm~D?hfM@qcqqF&HJm^+)oB`c)lj}X;kn?H7av53?yXa5Fl};|93CtZw%ND8erwboaKI-W# zJyA=6FOsZPa*-Wb7BGOEWOEI)6nE0FM1amrSC3jVt6vH;wSKE=`D0 zXXy?sQ8Es`#8}snu9R5VQvK5p;i)C=oK@2=cCx6{mE3kIm=yMK@An__mxf_F#_b|k z_p_DTCFB@3tv(LCl5?vDurlav zj=UXOhybJr{gY$7eKIq*|C!OFq9g|tYwBg{Vs>71reo_o6zBUM#>AXZKXzgNW{pN5 z8JCFp_?xd9YucLRK97N2<`C5XPGSAm2 zj!$u+p!-ROdFPRR2RJPSYta+*x>R#LOcZJ~fS?ron2w)8F!M@s$q4=^QB``Ey!`!- zuLbQ>`6qlMzi+$R#2rvqp}!w4hzKRTA5;TGy(Z@iDQ(to1f;Wewv|o1gmwSwqoz@~ z-id*A$?mR7a!(}kWuIbZ+CZ9_x*^P2e1KTM9X7kJIpy`?*RMm;0EOW=q0@0v`u1wp zr52oP5UHgFG0mnu&x_47SVV??UfL8gNUJ{R>%k@X68ga(LvURK3!4L2=rMg4>8Iy^ zfj{_(J=PhYN`vXKgCZvBRRuOniO*x#`N&7dWyoY0wL4Uwa;!TwlD(sU=;i(7=aU@v zH3bqyCKXHf=wG6~ZPDze9FDak`Y-%_K%181)(Lt#bL%Y&r0T^ru>A-JHw}_|iFqW8hu!wP(rA7kA#Bb+nS)jyvS%JVs#2 zz9R#pCqH)G7S_N>8Fe9zkKfIB-EEW5q>W2nOp2Yz35}KRvkrowESl`Y*C!^#ikhco zJg|i`Iwx&_wx)LRiQQP&o#oMTjPoWmlR@sum4#eB?koJ#9m>^(#sDr)3V^?iL9Wv< z&91Xtrss$`$D2h@36>wtT!7)}+@&v~Uz@(ShTz^~UF~AbYdNK079P_TU?9(Ba}mJLi7_p8qiy5(h8 zPG~U1QXJ+({uCGBQ0OMutj#N={}g1t4r40#i)BcjbT}OxUj;pX{0zIk;yZB~cRwX7De~dE?Jc3HZ;);9z-7Y;Z;`?|m%y2~>+ddrN*z(7`Xp z*mY6!GUGfoC`IA)fUBuQU;AeC#QxA_5mtXtm_kWbwqF<yXT zrI4cp=lqg-;8rZ3`%xHq$Yc<2pf%ouyH2VfLOl$W8#gpAOomheOjY^Z`(g(G zMdk8@MFd^8(rQUf)c9kE0q662j#_)%;(gXLesA*`D#Vi%^f|hRJ`b*!=~<`I~e%0L$Bl9TW=j77=se5yI1vu;vZp{x2Tke(Wg@PwmGA%}#AxF>#w=8hw?PsF>Vj zxY0Ui%-L25<%?%wgm_*fov%;3gBGZlKyk)Z5$2j)5|5Ux40cxm)YsAEAw@;QZpOb> zP5Lqcs%rQFweBa@vF@xjf%EDGU?yVeE=`K388Cqvm&tUvK3z=oK<&o!4EdC9j%vpW zpJh`l0H1KmMf$V~Xr;je@v>)?#2~Tm7Alhr;3p&CY>MB9X66cTh^F0Q zYibjz8i)r*FGJyOKwh&0Lr45lI7GCQ&y6Sbq@_tnNFpRaTYffcsQIh!2fH>$3TELh zXiSW_>5ks(qLPYUr*X%dp|MD@?BE~SSqv=TPLZtMFJr+)cVBsro`3@aLYL6bxwPuV zP27Q^xSx*70IYM(TCz5oT?daXL|ZGi@8)l=1@W82sJc#PrLk`v0i?2sq(LR=+{nZv z)vxu|x}qFR-h(-%W$wLJ@2sWRy^dQ>^+b~Jm0sn@8>G*f#H(B=sMRY-+xKl z*O7I%OHR|}ovZ1AB@Ws=LIJ3$N%vXxJbBf;ppfXbLcs#5OpG{eT|e6Qz_-zQmI&k1 zwt|7PBK_kba~VE%f~*9(4rf*=%40oB>%D{qkq=8b+Dqgz={q(;GSEj)=_OygsR&~D zH`n^h@mb~jd$Vpw(ha$AhtAQ-2N&p(XnZiWamv81u0hyzeed9jhH(vfFLS4sM?%Qj z@$z3xf_~0=>>2_9(otR92E1c48zua**4JRWRSsLtFE9G$0NinrXN52%Ga4y6cZI7Z zXjY^n+uxNZ+()SSejOiDwpYa;wkh<{Q^X$}-q?-a1Qa2+3-WtNUwY)0nryi~!DZ)N{{fkkD_BhqoXfV2JCQ;4k1u6Qnh+b%6$ zhl-IPEBxy6!P3iN;d1(B=P-w*@Xwh8IvqS6m7J~a71o!}_2QBQ8)F0JN3!cVO_1M~ zd~vw>kM~u;dNf?T4euLF7sdlXiR~k91`3fc-w#|1tn5{&NuRSpr9tmMQVnQvNT#(r zDFU)xVIb_}Xu}=HEnlbkN4F2#nYX~)AxCm9&iV84E};T36C7PY0cfQC92=Pi+R4eB zil$+^x7>Q@v2WWdtUs-IP`t$>^36-z_qMf@3!9k81^A*zkcv;h3T84vx{Reg|C|Y4 z`>oM5uBzTxrikyZ7t+hbuHa;ec$t6;vJ{xwsK?`SW$kWsLQ?174S!hr5aD(HyIkhw zeGlRvKL1`}`VuNjhfSw(4qF_O_q+`r%U1B3P*%w_SGYSDNiZlXyE=u1VCTPk0ZkzG zn%D`6`35apOy6&Y%V+s!V{KD2LZ9>=z>mLsAaF7g{X5m*VSB2Cb(V#omq9jHu4BBWc?;?&k9BrE?e|Ry=_6W#6RMT;qtQL{`KFjO?5&l5au{1~ zyudPK(F+X$@{leuv#PBseqgz{pe!!`)R=R@zd2`*)yrTsL>3 zQ<0Pw0bF4TgHA0wRGGriVe_S}7+dIV8hwATuqU+X!U%Fb>n=ZLCpJ~pc`O~W@4%@K^AHnW|eR2K2wz8Bp>an&f7 z&G7yRbOaF0T)0sNPT`2KhJI(Yln-~9uS1-?+z#){rMe?Vnez6DocwgrfFo_AWBf{d)~6}fg(#CWgM|PaK%OGP>VvQ zrzFOS$nCqaBoh>h_(2?+IFJj2{fSHLB%vE{L zy!*5+vDcCf>p??1K>0U`&hD>4X7fR@QcCe}^acS4oPs^eo>$N_7c;CUy@p&sIibWn zqD+y%svj%Jz6g@k*sd)2d0d)wYDYMqY;{a;v;k@3y{U=G?AXTKiFr& z$&gDtb%SToeR-49F*bzO*H|feD)?~+GbsTLoOQv;#ER4Ue7J0Nt!s=wrqmK}^VAMP4P`w0NLSq~0*T2_?jv zY5gGlR>)EeDqWIS%5S}GbYc3XoJGMAY`Co!&x7T=DZkz=lAA|M^CGpvTa)Np@3LEk zQ?@ll;iwxEQD0t{L|}WdRQ@I%2|w!-IPsfx>qtJ4#J1@D852*=PQs8~E&ZTcY3ok? zmwAk5nss2A{ST|EHM9L{7_$ug~%0Nhcgu{+ZaI194C-+7NA5b;=k z+>iJx(=Bye%eM)xWS(X^2Qe}@zBHSbFar>tG9#4!Tm_BJd!UcDhk7hjm7`}xcZw?% z>xysxYYZGgEJ(LWs7SwVeCeN*H&)3=Sikh1GacE$GL1a?6dDGk(%IQ=1ZO-1%ce=J zF*owevrx>=#eKUM{YroME2*$p0gPEFLt)Po?HlGe&xP>=MD?|u) z+ik3!{1kccg;)xeS!ESKXF3nF@ z>!aa@ad^75m#LXq2fqw#wGGS#rk!GrCSmZsdh$I2An}2Gu5&3H_|a@wQ!&#Mop8PR zk@A@N7w?EB-h;QP%cq5ZqgwykivBYX)cntUuKzww{6`767eHd))jsH2VaGz z{nO|lvWowGGWkzd?B5gkAKEMa-#_MRfOYs^-~Atk(pb6k>$x2L>~Y${1p8gH`%l5O z_L66?|C;WTtQ}e81{8qrxc{8^6EwKo+?aqys@)Ef3l4zuH2034aR;mv`G58HihdC0 z_^N45Q5Q7n|Df$BI%JG6qPrZ;jPN}7I=}bQBpM^|?OJ5y=N(@=95vo+Li6r7I?oZR zmzjRc9~_GQ%IQC79}DCY^UizKpc-Eb59OcyTsRq)^JIv9a}O(>gWufk-FGsy1{UhT z&TG$&0wY8NoeBll;`qD9v+fPkQsy=4uh9pG1fu2rh?I}x@i#+Kw)>ZF_%j$#Jl|*n zb!}F`TOwEc%|IF+IJhs> zUdKWwMg6BXZLizYcydDP{UTeA*V!gpL9iX)jiUm@MQD8EcHL+6>~A~{4WZv{5p+Ya z$@P{Bte0ry9A=KL-J9R*AeKJk@8{<<9vWT@T`Xq7EohZcQGQ47cy|iycMBFF8I9H> z{aAT*6B^|rM|77SkQSl-GcBqn!fNcP8kL0-r6!3MPu->L{XvbyEXAZhUW`6;=KzCw zMNqPZp#$Q0QGs$O?h_T_yoYV~vu{)Muc`SFLf<5%z~|4y?gOfa& z<~G?2#;j)VKYdC(3@^JJj}ML1+;oK8u;lRm8Np7}w7hPegLTyI{d%~@8MaU9!2y4j zS-G9}#!4(?UG;u!ln}WjdveL5hg4j+DIt|6_P<~G@cwKB?Y;k~XXL$)1^FxjikF=b zNB=()hW~dC|L_m%5%#6Yyx%pEFdu#)@o(>>zqj}(#lEYBmdwYEH7TN610uz5=Kn>3 zp9(u!`p>Fr$y#W1l81!coSFmW0Vp^sl)QawMHISwIeu^x?g>S%~K5IQqwbukhTd(V0GR-Ayqh8edyg z=n+JxOSoe1MLz&?Fy6(kWk3DfI9_NH~_n=yjVZLF!~lUz#z$-2k1bm=VM?;>|^hX9mku={IkH7AeF2!gkRvw{Z-g@wFHr%moYASoD=$| z8H{_}TCh4_H;(-a^Z4sEnSca(>_7VJe^97++1H%s@nvZIR5*}EJnk}sA5l4d+gX-X zShZWheg1Xt!f&lMJYQCFE(>Wuy;L}7L?k9F?n7OMdrbNREDBr?N^ty)@!&0SYO*K) zIdOwv{3Ily&^}0S2}yRt41^Rt%^aQ7>65H-{mAh6SA5JIm&FVM&AmSLn)_>->%Pt8 z_k6bnl?&VC@sh3i(P{sH9t7+SWN9;_O1(KuCcMpA$fjT*KF0blwkrILAG(j}U|}rE z+mPhBkazU$FICEOLG_SdyD#&wCLw)o(|H2VhKBQe5SZ6slgii0UmUGl8#F-s7o7d4 zc4kiJw4r2NpRIw6znvIP;0ov?e`lzudnKNYj3E2_&`B!jiw2Xz1fiN z%UHaA-Ra^IC$Q6)?ik_qPuZ5pNcM8snar_{S87QfSj019eJ3G;?RLv2hReT`YxCPS zdztg=&!1_$N{u-tfk(1UCgLLa>yF0)x&u24&|Q6{P3qy}2KBoKbt3%frj2#e#nthN zhgxA{6Co~K*alsY;tp8@JiHm)@d!p}?WoH7oYcg|vh38+ZZEV0qW&+NfQP5P7cp=- z-()`0wCbXzArxwunfL9mG>f**%CPo(i|MRAL9$IEOzk^cZQ4L>@j`4Y(2nyngS4Jm zbP<1)x48d!ME{DGis`BMN4#`X^TiwAx@mCN2>}zzLM?FrCQ7@-JwB7Uw(fN3$cTh7 zreSf!{(N09qIjbL=_CEmcE3ak94?i;^+wa1T99XP6ABB7*~os3u3FAEU>#C8{1YFl zG~nHjv%cBm+7?+U3o$*i$FE;Y+_z>f6Wai-FU^qexK}Bm{U&G%fiaWvs7+WX3%O_- zpDNc8eIbhSh~Y<9QTKOFvJk-{ILwB&(k7%J*r2o;>6~XvZS?(i@qy-DgBTHi!YgLx zVIAR6t4i4Wxl5;9^grP8NKArMusNF1dBbbhoPbVxs)?T0SLN8@`~>sZq*tW0%b^k99N#>MWEFf(|h1Nzzyx z9tyH?q)5=;%x|f!J4H?{AwcRs2<^UkbLBPto}%q;LX#3oP8&onf;UOIiPC>$Vh-sQ zXmDb{VCc>iXQBTvHaz#ZUt_pw(9L;3-?o#25Qu4grpOiZ-u15-E;F#i`A!aB+U~Q` z4fgKUiMgw-iK4!qkxc~~@tTJyIB(W^%bkdglA&xA@UHk0v>Gwwni!dZ^~Z{}jkaQi zUtGQWT&d^Ayq6ZdAlsNNAW$Qtjczjdx2IqzV?D4@<65XM&?Riv*pvfZZH?62B=jy;8hA}>qO0EE%)g8*9$(HXUw_1C2IlCAj)e z^?+7X+gwFoN2(~+MblTs(;(hQ4;r{6H<>sHG2ZZ{l3nNsqU-#_a;?WC1`T28=2cF? zjY|l7$n9eF_OCxH21-!U-(C1GzkSrvcjv!Y> literal 0 HcmV?d00001 diff --git a/docs/windows.md b/docs/windows.md index 1bbc847..43c178b 100644 --- a/docs/windows.md +++ b/docs/windows.md @@ -33,8 +33,11 @@ It runs immediately and at user sign-in, without elevation, and leaves bots running when the desktop closes. The task requires an interactive user session; it does not run while that user is signed out. Logs, configuration, and state live under `%USERPROFILE%\.gravity`; `GRAVITY_HOME` overrides this root. -If Windows reserves the preferred port (for example for Hyper-V), the local -managed daemon chooses an available port and the desktop follows it. Direct +The configured port defaults to 49777. If Windows reserves it (for example for +Hyper-V), the local managed daemon chooses an available port and the desktop +follows it. To use a fixed port, choose an available `port` in `gravityd.toml` +and restart the daemon. Remote clients and MCP URL allowlists must use the active +port. Direct launches and remotely reachable daemons require an available configured port. ```powershell From 3bd59d97d6e4d8358bfdd6fccbc998e383027a41 Mon Sep 17 00:00:00 2001 From: Manuel Rinaldi Date: Thu, 1 Oct 2026 15:32:26 +0300 Subject: [PATCH 11/55] fix(windows): tolerate stale daemon process IDs --- crates/gravityd/src/service/windows.rs | 31 +++++++++++++++++++------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/crates/gravityd/src/service/windows.rs b/crates/gravityd/src/service/windows.rs index 95c4b0b..1d829cc 100644 --- a/crates/gravityd/src/service/windows.rs +++ b/crates/gravityd/src/service/windows.rs @@ -98,6 +98,20 @@ fn run_task(args: &[&str]) -> anyhow::Result<()> { Ok(()) } +fn stop_daemon(pid: u32, executable: &Path) -> anyhow::Result<()> { + // A missing process leaves PowerShell's implicit exit status at 1 even + // with SilentlyContinue. An absent or reused PID needs no termination. + let script = format!( + "$p = Get-Process -Id {pid} -ErrorAction SilentlyContinue; if ($p -and $p.Path -eq {}) {{ & taskkill.exe /PID {pid} /T /F | Out-Null; exit $LASTEXITCODE }}; exit 0", + quote(executable) + ); + let out = Command::new("powershell.exe") + .args(["-NoProfile", "-NonInteractive", "-Command", &script]) + .output()?; + anyhow::ensure!(out.status.success(), "could not stop managed daemon"); + Ok(()) +} + fn stop(paths: &ServicePaths) -> anyhow::Result<()> { let name = task_name(paths)?; // Ending an idle task returns an error; the home lock below verifies stop. @@ -107,14 +121,7 @@ fn stop(paths: &ServicePaths) -> anyhow::Result<()> { if let Ok(pid) = std::fs::read_to_string(paths.pid_path()) { let pid: u32 = pid.trim().parse().context("invalid managed daemon PID")?; // Never kill a reused PID belonging to another executable. - let script = format!( - "$p = Get-Process -Id {pid} -ErrorAction SilentlyContinue; if ($p -and $p.Path -eq {}) {{ & taskkill.exe /PID {pid} /T /F | Out-Null; exit $LASTEXITCODE }}", - quote(&paths.bin_path()) - ); - let out = Command::new("powershell.exe") - .args(["-NoProfile", "-NonInteractive", "-Command", &script]) - .output()?; - anyhow::ensure!(out.status.success(), "could not stop managed daemon"); + stop_daemon(pid, &paths.bin_path())?; std::fs::remove_file(paths.pid_path())?; } // /end returns before Task Scheduler has finished ending the launcher. @@ -219,6 +226,14 @@ pub fn status(paths: &ServicePaths, configured_port: u16) -> bool { mod tests { use super::*; + #[test] + fn stopping_an_absent_or_reused_pid_is_a_no_op() { + let root = tempfile::tempdir().expect("temporary home"); + let executable = root.path().join("gravityd.exe"); + stop_daemon(i32::MAX as u32, &executable).expect("absent process"); + stop_daemon(std::process::id(), &executable).expect("unrelated process is preserved"); + } + #[test] fn uninstall_without_an_installed_daemon_is_a_no_op() { let root = tempfile::tempdir().expect("temporary home"); From bd633ad6faa568053bbda93401e05527f2a61054 Mon Sep 17 00:00:00 2001 From: Manuel Rinaldi Date: Thu, 1 Oct 2026 17:30:47 +0300 Subject: [PATCH 12/55] fix(supervisor): start each bot's runtime only once at a time Bot creation and the supervision tick could both enter start_bot for a new bot before either registered its session. With Codex, the second runtime resumed the thread the first had just created and failed with "thread already has an active writer"; the failure path then cleared the healthy first session, crashing the bot until the next retry. start_bot now claims the bot under the supervisor lock before launching anything, and releases the claim when it returns, so a concurrent caller backs off instead of starting a second runtime. Co-Authored-By: Claude Opus 5.5 --- crates/gravityd/src/supervisor.rs | 5 + crates/gravityd/src/supervisor/claim.rs | 148 ++++++++++++++++++++ crates/gravityd/src/supervisor/lifecycle.rs | 15 +- 3 files changed, 160 insertions(+), 8 deletions(-) create mode 100644 crates/gravityd/src/supervisor/claim.rs diff --git a/crates/gravityd/src/supervisor.rs b/crates/gravityd/src/supervisor.rs index da541e4..0910ecd 100644 --- a/crates/gravityd/src/supervisor.rs +++ b/crates/gravityd/src/supervisor.rs @@ -29,6 +29,7 @@ use crate::runtime::{BotSpec, RuntimeAdapter, RuntimeSession, SessionEvent}; use crate::secrets::Secrets; use crate::terminal::TermBuffer; +mod claim; mod hooks; mod lifecycle; mod restart; @@ -116,6 +117,9 @@ struct BotHandle { /// crash and the reconciler leaves the bot alone. stopping: bool, restart_pending: bool, + /// Set while one `start_bot` call is bringing the runtime up, so a second + /// caller does not launch another runtime against the same conversation. + starting: bool, consecutive_crashes: u32, last_start: Instant, /// Earliest time the reconciler may start this bot again; set while a @@ -146,6 +150,7 @@ impl BotHandle { repaint_generation: 0, stopping: false, restart_pending: false, + starting: false, consecutive_crashes: 0, last_start: Instant::now(), next_start_at: None, diff --git a/crates/gravityd/src/supervisor/claim.rs b/crates/gravityd/src/supervisor/claim.rs new file mode 100644 index 0000000..f0c8882 --- /dev/null +++ b/crates/gravityd/src/supervisor/claim.rs @@ -0,0 +1,148 @@ +//! One start at a time per bot. Bot creation starts a new bot right away, and +//! the supervision tick starts any bot without a session; a slow runtime +//! (Codex spins up an app server) leaves both inside `start_bot` together, and +//! two runtimes resuming one conversation crash each other. + +use super::*; + +/// Proof that this caller owns the bot's start. Dropping it hands the bot back +/// to the next start, whether this one brought a session up or failed. +pub(super) struct StartClaim<'a> { + sup: &'a Supervisor, + bot_id: &'a str, +} + +impl Supervisor { + /// Claims the bot for a start, or `None` when it is already running or + /// another start is in flight. + pub(super) fn claim_start<'a>(&'a self, bot_id: &'a str) -> Option> { + let mut bots = self.lock_bots(); + let handle = bots + .entry(bot_id.to_string()) + .or_insert_with(|| BotHandle::new(self.inner.cfg.scrollback_bytes)); + if handle.starting || (handle.session.is_some() && handle.state.is_running()) { + return None; + } + handle.starting = true; + Some(StartClaim { sup: self, bot_id }) + } +} + +impl Drop for StartClaim<'_> { + fn drop(&mut self) { + if let Some(handle) = self.sup.lock_bots().get_mut(self.bot_id) { + handle.starting = false; + } + } +} + +#[cfg(test)] +mod tests { + use std::sync::atomic::{AtomicUsize, Ordering}; + + use super::*; + use crate::overrides::AutoCompactOverride; + use crate::runtime::double::DoubleAdapter; + use crate::runtime::{Capabilities, StartedSession}; + + /// A runtime as slow to come up as Codex, counting how often it is asked. + struct SlowAdapter { + starts: AtomicUsize, + fail: bool, + } + + impl RuntimeAdapter for SlowAdapter { + fn capabilities(&self) -> Capabilities { + DoubleAdapter.capabilities() + } + + fn start(&self, spec: &BotSpec) -> anyhow::Result { + self.starts.fetch_add(1, Ordering::SeqCst); + std::thread::sleep(Duration::from_millis(200)); + if self.fail { + anyhow::bail!("runtime refused to start"); + } + DoubleAdapter.start(spec) + } + + fn probe(&self) -> anyhow::Result { + DoubleAdapter.probe() + } + } + + fn supervisor(home: &std::path::Path, adapter: Arc) -> (Supervisor, String) { + let cfg = Config { + home: home.to_path_buf(), + ..Config::default() + }; + let db = Db::open(&home.join("bus.sqlite")).expect("db"); + let project = db.create_project("p", "p").expect("project"); + let workspace = home.join("p/bots/alice/workspace"); + let bot = db + .create_bot( + &project.id, + "alice", + "", + "", + "", + &workspace.display().to_string(), + "alice", + None, + ) + .expect("bot"); + let secrets = Arc::new(Secrets::open(&home.join("secrets")).expect("secrets")); + let sup = Supervisor::new( + adapter, + cfg, + db, + Events::new(), + secrets, + AutoCompactOverride::default(), + ); + (sup, bot.id) + } + + async fn start_concurrently(sup: &Supervisor, bot_id: &str, callers: usize) { + let tasks: Vec<_> = (0..callers) + .map(|_| { + let sup = sup.clone(); + let bot_id = bot_id.to_string(); + tokio::task::spawn_blocking(move || sup.start_bot(&bot_id)) + }) + .collect(); + for task in tasks { + // A failing runtime fails only the caller that owned the start. + let _ = task.await.expect("start task"); + } + } + + #[tokio::test(flavor = "multi_thread")] + async fn concurrent_starts_launch_one_runtime() { + let home = tempfile::tempdir().expect("tempdir"); + let adapter = Arc::new(SlowAdapter { + starts: AtomicUsize::new(0), + fail: false, + }); + let (sup, bot_id) = supervisor(home.path(), adapter.clone()); + + start_concurrently(&sup, &bot_id, 4).await; + + assert_eq!(adapter.starts.load(Ordering::SeqCst), 1); + assert!(sup.has_session(&bot_id)); + } + + #[tokio::test(flavor = "multi_thread")] + async fn a_failed_start_releases_the_bot_for_the_next() { + let home = tempfile::tempdir().expect("tempdir"); + let adapter = Arc::new(SlowAdapter { + starts: AtomicUsize::new(0), + fail: true, + }); + let (sup, bot_id) = supervisor(home.path(), adapter.clone()); + + assert!(sup.start_bot(&bot_id).is_err()); + assert!(sup.start_bot(&bot_id).is_err()); + + assert_eq!(adapter.starts.load(Ordering::SeqCst), 2); + } +} diff --git a/crates/gravityd/src/supervisor/lifecycle.rs b/crates/gravityd/src/supervisor/lifecycle.rs index f13d79c..98328cf 100644 --- a/crates/gravityd/src/supervisor/lifecycle.rs +++ b/crates/gravityd/src/supervisor/lifecycle.rs @@ -10,14 +10,12 @@ impl Supervisor { pub fn start_bot(&self, bot_id: &str) -> anyhow::Result<()> { let bot = self.inner.db.get_bot(bot_id)?.context("bot not found")?; - { - let bots = self.lock_bots(); - if let Some(h) = bots.get(bot_id) { - if h.session.is_some() && h.state.is_running() { - return Ok(()); // already running - } - } - } + // Creation and the supervision tick can both start a new bot. Held + // until this call returns, so a second start finds the claim and + // backs off instead of fighting this one over the same conversation. + let Some(_claim) = self.claim_start(bot_id) else { + return Ok(()); // already running, or another start is in flight + }; let token = self.inner.secrets.bot_token(bot_id)?; let workspace = std::path::PathBuf::from(&bot.workspace_path); let bot_root = workspace.parent().map(|p| p.to_path_buf()); @@ -318,6 +316,7 @@ impl Supervisor { None => true, Some(h) => { h.session.is_none() + && !h.starting && !h.stopping && h.next_start_at.is_none_or(|at| Instant::now() >= at) } From 205e4a418cde3fa95b4db6c2fcf9e82b5a99cab9 Mon Sep 17 00:00:00 2001 From: Manuel Rinaldi Date: Thu, 1 Oct 2026 17:55:01 +0300 Subject: [PATCH 13/55] docs: design peer bots across two daemons Co-Authored-By: Claude Opus 5.5 --- docs/peer-bots.md | 227 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 docs/peer-bots.md diff --git a/docs/peer-bots.md b/docs/peer-bots.md new file mode 100644 index 0000000..b40de06 --- /dev/null +++ b/docs/peer-bots.md @@ -0,0 +1,227 @@ +# Peer bots: one team across two machines + +Status: design, not yet implemented. + +A team can span two Gravity daemons. The motivating case is an app built for +macOS and Windows: a lead and a Mac developer run on the Mac, a Windows +developer runs on a Windows PC, and the lead hands the Windows side its work +without the user switching the desktop app between daemons to relay messages +or fetch files. + +Each bot still runs, with its own terminal and runtime, on the machine that +owns it. What crosses machines is the bus: messages, tasks, results, and the +artifacts those results attach. + +## Assumptions + +- Both daemons belong to one owner and reach each other over Tailscale. Like + remote desktop clients today, the link is plain `ws://` on the tailnet; the + tailnet is the transport security, and a peer token is the authentication. +- Two machines is the target. Nothing below limits a daemon to one peer, but + multi-peer routing (A relaying for B to C) is out of scope: messages only + ever travel one link. +- The desktop UI is due for a rework, so this version adds the minimum UI to + pair, link, and tell a linked bot apart, and leaves the rest for the rework. + +## Concepts + +**Peer.** Another daemon this one is paired with. Stored in a `peer` table: +local id, display name (`win-pc`), the remote daemon's stable id, the URL to +dial if this side dials, and `last_seen_at`. Each daemon gets a stable +`daemon_id` in `meta` the first time it starts. + +**Linked bot.** A `bot` row whose `peer_id` is set. It stands in for a bot +that runs on the peer, with the peer's id for it in `remote_bot_id`. It has no +workspace and no runtime: the supervisor never starts it. Everything else +treats it as a bot in the project. It appears in `list_bots`, is addressed by +name, owns a DM conversation, and is the `from`/`to` of task rows. Because it is +a real row, every foreign key and guardrail that assumes a local bot keeps +working unchanged. + +Linking is symmetric. When the Mac lead first messages the Windows developer, +the Windows daemon creates a linked bot for the lead in the Windows +developer's project. The Windows developer's replies and results are then +ordinary sends to a local bot, which the Windows delivery worker forwards +back. + +## Pairing + +1. On the Windows daemon (which must bind its Tailscale address, as for any + remote client), the owner creates an invite. It returns a one-time code + with the daemon's tailnet URL and a fresh peer token. +2. On the Mac, the owner adds the peer with that code. The Mac daemon stores + the URL and token (`secrets/peer-.token`), dials, and both sides + record each other's `daemon_id` and name. +3. Revoking a peer on either side deletes its token, and that side's linked + bots stop accepting deliveries (they fail with "peer revoked"). + +The dialing side holds one long-lived WebSocket to `/peer` on the other, and +traffic flows both ways over it. Only the listening daemon needs to be +reachable. The dialer reconnects with backoff, and `last_seen_at` drives the +online/offline badge. + +A peer token authenticates only the `/peer` route. It carries no control-plane +grants: a peer cannot list projects, attach terminals, rule on decisions, or +reach a bot that has not been linked to it. + +## Linking a bot + +The owner picks a project on the Mac, a peer, and one of the peer's bots (the +peer answers a `list_bots` frame with id, name, description, and runtime). The +Mac creates the linked bot under the remote bot's name. If the name is already +taken in the project, the link is refused until one of the bots is renamed, +since names are what bots address each other by. + +The peer records the link: the remote bot accepts frames from this peer only +once it is linked. That is the exposure boundary. Linking a bot exposes that +one bot to the peer, and nothing else in its project. + +Unlinking archives the linked bot locally, like deleting any bot: its open +tasks are cancelled and its history is kept. + +## Message flow + +Sending is unchanged up to the delivery worker. `send_message`, `send_user_message`, +`complete_task`, and `cancel_task` insert messages, open or close task rows, and +enqueue a delivery to the recipient bot. The single new branch is in +`DeliveryWorker::attempt`: when the recipient is a linked bot, the worker +forwards the message over the peer link instead of calling +`Supervisor::deliver`. + +- Peer offline: `NotReady`, so the delivery waits without spending attempts, + exactly like a bot that is still starting. +- Peer rejected the frame (bot unlinked, task unknown, peer revoked): + `Failed`, with the peer's reason as the delivery error. +- Peer acknowledged: `delivered`. The ack means the peer has durably stored + the message and enqueued its own local delivery. It does not mean the remote + bot has read it, which matches what `delivered` means locally. + +A forwarded frame carries: + +```json +{ + "type": "message", + "id": "", + "to_bot_id": "", + "from": { "kind": "bot", "bot_id": "", "name": "lead" }, + "kind": "task | reply | note | done | chat", + "body": "…", + "ref": "", + "task": { "id": "", "deadline_at": "…", "hop_count": 2 }, + "closes_task": { "id": "", "state": "done | cancelled" }, + "artifacts": [{ "name": "build.log", "bytes": "" }] +} +``` + +The receiver handles it in one transaction: + +1. **Dedupe.** `(peer_id, id)` in `peer_message` means it was already stored, + so the receiver acks again and does nothing else. +2. **Resolve the recipient.** `to_bot_id` must be a live, local, non-linked + bot linked to this peer. +3. **Resolve the sender.** `kind: bot` uses the linked bot for + `(peer, from.bot_id)`, creating it in the recipient's project on first + contact. `kind: user` is the owner speaking from the other machine; it + arrives as `chat` with the user's authority, which is the reason peer + tokens are only issued between the owner's own daemons. +4. **Insert and enqueue** with `messaging::send_dm`, mapping `ref` through + `peer_message` so threading survives the hop. +5. **Mirror the task.** A `task` frame opens a local task from the linked + sender to the recipient. It keeps the sender's `deadline_at`, and its + `hop_count` is the sender's, so the hop limit holds across the whole chain. + `peer_task` maps the two task ids. A `closes_task` frame flips the mapped + local task (`try_close_task`) before inserting the `done` or cancel note, + exactly as the local tool would. + +Replies count against the reply budget on both sides, because each side holds +its own copy of the task. Loops are refused locally by the existing chain +check: the Windows developer cannot delegate back to the lead's linked bot +while working the lead's task, because that linked bot is already in the +chain. Deadlines expire independently on both sides from the same timestamp. + +### Artifacts + +`complete_task` already lists artifact paths in the `done` body. When the +recipient is a linked bot, the forwarder also reads each listed file and +sends its bytes in the frame. Only regular files under the sending project's +artifacts directory or the sending bot's workspace are sent. The cap is +16 MiB per file and 48 MiB per message. A file over the cap stays listed by its +remote path, with a note saying it was not transferred. + +The receiver writes the files to +`/peers///` and rewrites the listing in +the stored body to those local paths. The lead reads the result exactly as it +would read a local one. + +## What bots are told + +- `list_bots` adds `"machine": ""` and `"online": bool` for linked + bots. +- The envelope header names the machine: `from windows-dev @ win-pc`. The + sender is still daemon-authenticated, but the authority is a peer bot's, + never the user's, unless the frame came from the owner (`kind: user`). +- The system prompt's bus section says a linked bot runs on another machine + and that paths it mentions outside a transferred artifact are not readable + here. + +## Desktop app (minimum for this version) + +- Settings → Peers: create an invite, add a peer from a code, see each peer's + status, and revoke a peer. +- Project → Link a bot from a peer. +- A linked bot shows its machine and online state in the bot list. Selecting + it shows its conversation and the composer instead of a terminal. Sending to + it from the composer is a forwarded `chat`. + +Watching the Windows bot's terminal from the Mac is left to the UI rework. +It needs the app to hold connections to two daemons at once. + +## Schema + +One append-only migration: + +```sql +CREATE TABLE peer ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + daemon_id TEXT UNIQUE, -- learned at first handshake + url TEXT, -- set on the dialing side + created_at TEXT NOT NULL, + last_seen_at TEXT, + revoked_at TEXT +); +ALTER TABLE bot ADD COLUMN peer_id TEXT REFERENCES peer(id); +ALTER TABLE bot ADD COLUMN remote_bot_id TEXT; +CREATE UNIQUE INDEX idx_bot_remote ON bot(peer_id, remote_bot_id, project_id) + WHERE peer_id IS NOT NULL AND deleted_at IS NULL; + +CREATE TABLE peer_link ( -- local bots exposed to a peer + peer_id TEXT NOT NULL REFERENCES peer(id), + bot_id TEXT NOT NULL REFERENCES bot(id), + PRIMARY KEY (peer_id, bot_id) +); +CREATE TABLE peer_message ( -- dedupe and ref mapping + peer_id TEXT NOT NULL REFERENCES peer(id), + remote_message_id TEXT NOT NULL, + message_id TEXT NOT NULL, + PRIMARY KEY (peer_id, remote_message_id) +); +CREATE TABLE peer_task ( -- the two halves of a mirrored task + peer_id TEXT NOT NULL REFERENCES peer(id), + remote_task_id TEXT NOT NULL, + task_id TEXT NOT NULL, + PRIMARY KEY (peer_id, remote_task_id) +); +``` + +## Delivery plan + +1. **Peers and forwarding (daemon).** The schema, `daemon_id`, pairing and + revocation as control-plane requests (plus `gravityd peer …` commands that + drive the local daemon, so this is usable before the UI lands), the `/peer` + route and dialer, linking, and forwarding of every message kind with task + mirroring. Tested with two in-process daemons on the double runtime. +2. **Artifacts.** File transfer on `done`, with path rewriting and the caps. +3. **Desktop app.** The Peers settings, link flow, and linked-bot view. +4. **Live check.** Mac and Windows over Tailscale with real runtimes, then a + `bus-live-test` mode for two daemons. From b9da2949d06c9c9bfc59c636a540d7251145acd1 Mon Sep 17 00:00:00 2001 From: Manuel Rinaldi Date: Thu, 1 Oct 2026 18:11:18 +0300 Subject: [PATCH 14/55] feat(peer): link bots across two daemons Pair two of the owner's daemons over Tailscale and stand a bot from one in as a linked bot in a project on the other. A linked bot is an ordinary bot row with peer_id set, so addressing, task budgets and history work unchanged; the delivery worker forwards deliveries for it over the /peer link instead of writing to a local session. The receiving daemon stores each forwarded message as if the sender were local: a task is mirrored with the sender's deadline and hop count, a result or cancel closes the mirrored task, and the sender gets a linked bot of its own on first contact so replies route back. Files listed in a result travel with it (from the bot's own directory or the project artifacts only, size-capped) and land in the recipient project's artifacts directory. Pairing is a one-time invite holding a link token bound to the first daemon that presents it. Peers can only list bots, link one, and deliver to bots linked to them; revoking drops the link. `gravityd peer ...` drives pairing and linking until the desktop UI lands. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 1 + crates/bus/src/schema.rs | 3 + crates/bus/src/schema/peers.rs | 46 +++ crates/bus/src/types.rs | 4 + crates/bus/src/types/entities.rs | 72 +---- crates/bus/src/types/peers.rs | 44 +++ crates/bus/src/types/revisions.rs | 64 ++++ crates/gravityd/Cargo.toml | 1 + crates/gravityd/src/app.rs | 4 + crates/gravityd/src/botmgmt.rs | 11 +- crates/gravityd/src/db.rs | 1 + crates/gravityd/src/db/bot_runtime.rs | 2 + crates/gravityd/src/db/bots.rs | 10 +- crates/gravityd/src/db/peers.rs | 292 ++++++++++++++++++ crates/gravityd/src/delivery.rs | 90 ++++-- crates/gravityd/src/lib.rs | 1 + crates/gravityd/src/main.rs | 4 + crates/gravityd/src/mcp/tools.rs | 14 +- crates/gravityd/src/paths/prompt.rs | 6 + crates/gravityd/src/peer.rs | 19 ++ crates/gravityd/src/peer/artifacts.rs | 183 +++++++++++ crates/gravityd/src/peer/cli.rs | 197 ++++++++++++ crates/gravityd/src/peer/forward.rs | 188 ++++++++++++ crates/gravityd/src/peer/frames.rs | 74 +++++ crates/gravityd/src/peer/hub.rs | 182 +++++++++++ crates/gravityd/src/peer/inbound.rs | 76 +++++ crates/gravityd/src/peer/receive.rs | 202 ++++++++++++ crates/gravityd/src/peer/socket.rs | 237 ++++++++++++++ crates/gravityd/src/secrets.rs | 56 ++++ crates/gravityd/src/server.rs | 4 + crates/gravityd/src/supervisor/lifecycle.rs | 5 +- crates/gravityd/src/ws.rs | 1 + crates/gravityd/src/ws/dispatch.rs | 8 + crates/gravityd/src/ws/peers.rs | 197 ++++++++++++ crates/gravityd/src/ws/views.rs | 24 +- crates/gravityd/tests/peer_bots.rs | 322 ++++++++++++++++++++ docs/architecture.md | 4 +- docs/peer-bots.md | 29 +- docs/protocol.md | 6 + third-party/inventory.json | 4 +- 40 files changed, 2594 insertions(+), 94 deletions(-) create mode 100644 crates/bus/src/schema/peers.rs create mode 100644 crates/bus/src/types/peers.rs create mode 100644 crates/bus/src/types/revisions.rs create mode 100644 crates/gravityd/src/db/peers.rs create mode 100644 crates/gravityd/src/peer.rs create mode 100644 crates/gravityd/src/peer/artifacts.rs create mode 100644 crates/gravityd/src/peer/cli.rs create mode 100644 crates/gravityd/src/peer/forward.rs create mode 100644 crates/gravityd/src/peer/frames.rs create mode 100644 crates/gravityd/src/peer/hub.rs create mode 100644 crates/gravityd/src/peer/inbound.rs create mode 100644 crates/gravityd/src/peer/receive.rs create mode 100644 crates/gravityd/src/peer/socket.rs create mode 100644 crates/gravityd/src/ws/peers.rs create mode 100644 crates/gravityd/tests/peer_bots.rs diff --git a/Cargo.lock b/Cargo.lock index 734722b..600e137 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -520,6 +520,7 @@ version = "0.13.0" dependencies = [ "anyhow", "axum", + "base64", "bus", "bytes", "chrono", diff --git a/crates/bus/src/schema.rs b/crates/bus/src/schema.rs index 10283ed..89faee7 100644 --- a/crates/bus/src/schema.rs +++ b/crates/bus/src/schema.rs @@ -7,6 +7,7 @@ mod base; mod decisions; mod history; +mod peers; use base::MIGRATION_1; use decisions::MIGRATION_12; @@ -14,6 +15,7 @@ use history::{ MIGRATION_10, MIGRATION_11, MIGRATION_2, MIGRATION_3, MIGRATION_4, MIGRATION_5, MIGRATION_6, MIGRATION_7, MIGRATION_8, MIGRATION_9, }; +use peers::MIGRATION_14; pub const MIGRATIONS: &[&str] = &[ MIGRATION_1, @@ -29,4 +31,5 @@ pub const MIGRATIONS: &[&str] = &[ MIGRATION_11, MIGRATION_12, "ALTER TABLE bot ADD COLUMN runtime TEXT NOT NULL DEFAULT 'claude_code' CHECK(runtime IN ('claude_code', 'codex_cli'));", + MIGRATION_14, ]; diff --git a/crates/bus/src/schema/peers.rs b/crates/bus/src/schema/peers.rs new file mode 100644 index 0000000..511f916 --- /dev/null +++ b/crates/bus/src/schema/peers.rs @@ -0,0 +1,46 @@ +//! Migration 14: peer daemons and linked bots. +//! +//! A linked bot is an ordinary `bot` row with `peer_id` set, so every foreign +//! key that assumes a local bot (message senders, task ends, deliveries) keeps +//! holding. The mapping tables carry the other side's ids: a message or task +//! that crosses the link has an id on each daemon, and threading, dedupe and +//! closing a mirrored task all need to translate between them. + +pub(super) const MIGRATION_14: &str = r#" +CREATE TABLE peer ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + daemon_id TEXT UNIQUE, + url TEXT, + created_at TEXT NOT NULL, + last_seen_at TEXT, + revoked_at TEXT +); + +ALTER TABLE bot ADD COLUMN peer_id TEXT REFERENCES peer(id); +ALTER TABLE bot ADD COLUMN remote_bot_id TEXT; +CREATE UNIQUE INDEX idx_bot_remote ON bot(peer_id, remote_bot_id, project_id) + WHERE peer_id IS NOT NULL AND deleted_at IS NULL; + +CREATE TABLE peer_link ( + peer_id TEXT NOT NULL REFERENCES peer(id), + bot_id TEXT NOT NULL REFERENCES bot(id), + PRIMARY KEY (peer_id, bot_id) +); + +CREATE TABLE peer_message ( + peer_id TEXT NOT NULL REFERENCES peer(id), + remote_message_id TEXT NOT NULL, + message_id TEXT NOT NULL, + PRIMARY KEY (peer_id, remote_message_id) +); +CREATE INDEX idx_peer_message_local ON peer_message(peer_id, message_id); + +CREATE TABLE peer_task ( + peer_id TEXT NOT NULL REFERENCES peer(id), + remote_task_id TEXT NOT NULL, + task_id TEXT NOT NULL, + PRIMARY KEY (peer_id, remote_task_id) +); +CREATE INDEX idx_peer_task_local ON peer_task(peer_id, task_id); +"#; diff --git a/crates/bus/src/types.rs b/crates/bus/src/types.rs index 3fc3b5b..e558bfc 100644 --- a/crates/bus/src/types.rs +++ b/crates/bus/src/types.rs @@ -7,11 +7,15 @@ mod decision_enums; mod decisions; mod entities; mod enums; +mod peers; +mod revisions; pub use decision_enums::*; pub use decisions::*; pub use entities::*; pub use enums::*; +pub use peers::*; +pub use revisions::*; pub type Id = String; diff --git a/crates/bus/src/types/entities.rs b/crates/bus/src/types/entities.rs index a86827a..5fc2d14 100644 --- a/crates/bus/src/types/entities.rs +++ b/crates/bus/src/types/entities.rs @@ -51,9 +51,23 @@ pub struct Bot { /// leave `list_bots`, addressing, and the population cap. #[serde(skip_serializing_if = "Option::is_none")] pub deleted_at: Option>, + /// Set on a linked bot: one that runs on this peer daemon and stands in + /// here as a message target. It has no workspace and no runtime. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub peer_id: Option, + /// The linked bot's id on its peer. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub remote_bot_id: Option, pub created_at: DateTime, } +impl Bot { + /// True for a bot that runs on a peer daemon rather than here. + pub fn is_linked(&self) -> bool { + self.peer_id.is_some() + } +} + #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum BotRuntime { @@ -71,64 +85,6 @@ impl BotRuntime { } } -/// One field-level change to a bot's identity: the audit trail that makes -/// unsupervised self-management reversible. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BotRevision { - pub id: Id, - pub bot_id: Id, - /// `user` or `bot:`. - pub changed_by: String, - pub field: RevisionField, - pub old_value: String, - pub new_value: String, - pub created_at: DateTime, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum RevisionField { - Name, - Avatar, - Description, - Instructions, - /// Lifecycle markers rather than field edits; `new_value` carries the - /// reason, so a bot's whole life is one ordered list. - Created, - Deleted, -} - -impl RevisionField { - pub fn as_str(self) -> &'static str { - match self { - RevisionField::Name => "name", - RevisionField::Avatar => "avatar", - RevisionField::Description => "description", - RevisionField::Instructions => "instructions", - RevisionField::Created => "created", - RevisionField::Deleted => "deleted", - } - } - - pub fn parse(s: &str) -> Option { - match s { - "name" => Some(RevisionField::Name), - "avatar" => Some(RevisionField::Avatar), - "description" => Some(RevisionField::Description), - "instructions" => Some(RevisionField::Instructions), - "created" => Some(RevisionField::Created), - "deleted" => Some(RevisionField::Deleted), - _ => None, - } - } - - /// Whether reverting this entry means writing `old_value` back. Lifecycle - /// markers are history, not state, so they are not revertible. - pub fn is_revertible(self) -> bool { - !matches!(self, RevisionField::Created | RevisionField::Deleted) - } -} - /// A bot's DM thread. Every bot has exactly one, created with it. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Conversation { diff --git a/crates/bus/src/types/peers.rs b/crates/bus/src/types/peers.rs new file mode 100644 index 0000000..dc79b28 --- /dev/null +++ b/crates/bus/src/types/peers.rs @@ -0,0 +1,44 @@ +//! Peer daemons: another Gravity daemon of the same owner whose bots this +//! one can exchange messages with. See `docs/peer-bots.md`. + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +use super::Id; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Peer { + pub id: Id, + /// The owner's name for the other machine, e.g. `win-pc`. + pub name: String, + /// The other daemon's stable id, learned at the first handshake. A token + /// is bound to the first daemon that presents it. + pub daemon_id: Option, + /// Where to dial, on the side that dials. `None` on the side that listens. + pub url: Option, + pub created_at: DateTime, + pub last_seen_at: Option>, + pub revoked_at: Option>, +} + +/// Largest file sent with a forwarded result. +pub const MAX_PEER_ARTIFACT_BYTES: u64 = 16 * 1024 * 1024; + +/// Largest total of files sent with one forwarded result. +pub const MAX_PEER_ARTIFACTS_TOTAL_BYTES: u64 = 48 * 1024 * 1024; + +/// A bot as a peer describes it: enough to stand it in as a linked bot. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RemoteBot { + pub id: Id, + pub name: String, + #[serde(default)] + pub description: String, + #[serde(default)] + pub avatar: String, + #[serde(default)] + pub runtime: super::BotRuntime, + /// The project it works in on its own daemon. Informational only. + #[serde(default)] + pub project: String, +} diff --git a/crates/bus/src/types/revisions.rs b/crates/bus/src/types/revisions.rs new file mode 100644 index 0000000..55040be --- /dev/null +++ b/crates/bus/src/types/revisions.rs @@ -0,0 +1,64 @@ +//! The bot identity audit trail. + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +use super::Id; + +/// One field-level change to a bot's identity: the audit trail that makes +/// unsupervised self-management reversible. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BotRevision { + pub id: Id, + pub bot_id: Id, + /// `user` or `bot:`. + pub changed_by: String, + pub field: RevisionField, + pub old_value: String, + pub new_value: String, + pub created_at: DateTime, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RevisionField { + Name, + Avatar, + Description, + Instructions, + /// Lifecycle markers rather than field edits; `new_value` carries the + /// reason, so a bot's whole life is one ordered list. + Created, + Deleted, +} + +impl RevisionField { + pub fn as_str(self) -> &'static str { + match self { + RevisionField::Name => "name", + RevisionField::Avatar => "avatar", + RevisionField::Description => "description", + RevisionField::Instructions => "instructions", + RevisionField::Created => "created", + RevisionField::Deleted => "deleted", + } + } + + pub fn parse(s: &str) -> Option { + match s { + "name" => Some(RevisionField::Name), + "avatar" => Some(RevisionField::Avatar), + "description" => Some(RevisionField::Description), + "instructions" => Some(RevisionField::Instructions), + "created" => Some(RevisionField::Created), + "deleted" => Some(RevisionField::Deleted), + _ => None, + } + } + + /// Whether reverting this entry means writing `old_value` back. Lifecycle + /// markers are history, not state, so they are not revertible. + pub fn is_revertible(self) -> bool { + !matches!(self, RevisionField::Created | RevisionField::Deleted) + } +} diff --git a/crates/gravityd/Cargo.toml b/crates/gravityd/Cargo.toml index 4ffca78..4f9e980 100644 --- a/crates/gravityd/Cargo.toml +++ b/crates/gravityd/Cargo.toml @@ -29,6 +29,7 @@ hex = { workspace = true } futures = { workspace = true } bytes = { workspace = true } tokio-tungstenite = "0.24" +base64 = "0.22" [dev-dependencies] tempfile = "3" diff --git a/crates/gravityd/src/app.rs b/crates/gravityd/src/app.rs index be3448a..5713890 100644 --- a/crates/gravityd/src/app.rs +++ b/crates/gravityd/src/app.rs @@ -39,6 +39,9 @@ pub struct AppState { /// Runtime override for `cfg.auto_compact_window`, shared with the /// supervisor and persisted in the `meta` table. pub auto_compact: AutoCompactOverride, + /// Live links to peer daemons, shared by the delivery worker (forwarding + /// to linked bots) and the control plane (pairing and linking). + pub peers: crate::peer::PeerHub, pub started_at: Instant, /// Wall-clock start, kept alongside the monotonic `started_at` purely so /// [`AppState::stale_build`] can compare it against a file mtime. @@ -75,6 +78,7 @@ impl AppState { secrets, supervisor, auto_compact, + peers: crate::peer::PeerHub::default(), started_at: Instant::now(), started_wall: SystemTime::now(), })) diff --git a/crates/gravityd/src/botmgmt.rs b/crates/gravityd/src/botmgmt.rs index ca2fb7c..69a7659 100644 --- a/crates/gravityd/src/botmgmt.rs +++ b/crates/gravityd/src/botmgmt.rs @@ -185,6 +185,10 @@ pub fn apply_identity_edit( /// Rewrite the daemon-owned files for a bot after its identity changed. pub fn reprovision(app: &Arc, bot: &Bot) -> anyhow::Result<()> { + // A linked bot's files live on its peer; there is nothing to write here. + if bot.is_linked() { + return Ok(()); + } let project = app .db .get_project(&bot.project_id)? @@ -199,7 +203,12 @@ pub fn reprovision(app: &Arc, bot: &Bot) -> anyhow::Result<()> { /// `CLAUDE.md` pick up the new layout. The write is content-hash guarded, so /// this is a no-op once each bot is current. pub fn regenerate_all_system_md(app: &Arc) -> anyhow::Result<()> { - for bot in app.db.list_bots(None)? { + for bot in app + .db + .list_bots(None)? + .into_iter() + .filter(|b| !b.is_linked()) + { if let Err(e) = reprovision(app, &bot) { tracing::warn!(bot_id = %bot.id, error = %e, "system.md regeneration failed"); } diff --git a/crates/gravityd/src/db.rs b/crates/gravityd/src/db.rs index 4bdafd5..227adc2 100644 --- a/crates/gravityd/src/db.rs +++ b/crates/gravityd/src/db.rs @@ -26,6 +26,7 @@ mod decisions_list; mod decisions_tests; mod deliveries; mod devices; +mod peers; mod projects; mod revisions; mod routines; diff --git a/crates/gravityd/src/db/bot_runtime.rs b/crates/gravityd/src/db/bot_runtime.rs index aa1698b..248cdcc 100644 --- a/crates/gravityd/src/db/bot_runtime.rs +++ b/crates/gravityd/src/db/bot_runtime.rs @@ -57,6 +57,8 @@ impl Db { dir_name: dir_name.to_string(), created_by_bot_id: created_by_bot_id.map(|s| s.to_string()), deleted_at: None, + peer_id: None, + remote_bot_id: None, created_at: now(), }; let conn = self.lock(); diff --git a/crates/gravityd/src/db/bots.rs b/crates/gravityd/src/db/bots.rs index 0241812..2782fa2 100644 --- a/crates/gravityd/src/db/bots.rs +++ b/crates/gravityd/src/db/bots.rs @@ -18,7 +18,7 @@ const TOMBSTONE_SEP: char = '#'; impl Db { // ---- bots ---- - fn bot_from_row(r: &Row<'_>) -> rusqlite::Result { + pub(super) fn bot_from_row(r: &Row<'_>) -> rusqlite::Result { Ok(Bot { id: r.get(0)?, project_id: r.get(1)?, @@ -35,6 +35,8 @@ impl Db { "codex_cli" => BotRuntime::CodexCli, _ => BotRuntime::ClaudeCode, }, + peer_id: r.get(12)?, + remote_bot_id: r.get(13)?, // Runtime fields are overlaid by the supervisor. state: BotState::Stopped, state_reason: String::new(), @@ -42,8 +44,10 @@ impl Db { }) } - const BOT_COLS: &'static str = "id, project_id, name, description, avatar, instructions, \ - workspace_path, created_at, dir_name, created_by_bot_id, deleted_at, runtime"; + pub(super) const BOT_COLS: &'static str = + "id, project_id, name, description, avatar, instructions, \ + workspace_path, created_at, dir_name, created_by_bot_id, deleted_at, runtime, peer_id, \ + remote_bot_id"; /// Restricts a query to bots that still exist for addressing purposes. const LIVE: &'static str = "deleted_at IS NULL"; diff --git a/crates/gravityd/src/db/peers.rs b/crates/gravityd/src/db/peers.rs new file mode 100644 index 0000000..aa7a391 --- /dev/null +++ b/crates/gravityd/src/db/peers.rs @@ -0,0 +1,292 @@ +//! Peer daemons, linked bots, and the id mappings for messages and tasks that +//! cross a peer link. See `docs/peer-bots.md`. + +use bus::*; +use rusqlite::{params, OptionalExtension, Row}; + +use super::{parse_ts, ts, Db}; + +/// `meta` key holding this daemon's stable id, which peers bind a token to. +const DAEMON_ID_KEY: &str = "daemon_id"; + +impl Db { + // ---- this daemon ---- + + /// This daemon's stable id, created the first time it is asked for. + pub fn daemon_id(&self) -> anyhow::Result { + if let Some(id) = self.get_meta(DAEMON_ID_KEY)? { + return Ok(id); + } + let id = new_id(); + self.set_meta(DAEMON_ID_KEY, &id)?; + Ok(id) + } + + // ---- peers ---- + + fn peer_from_row(r: &Row<'_>) -> rusqlite::Result { + Ok(Peer { + id: r.get(0)?, + name: r.get(1)?, + daemon_id: r.get(2)?, + url: r.get(3)?, + created_at: parse_ts(&r.get::<_, String>(4)?), + last_seen_at: r.get::<_, Option>(5)?.map(|s| parse_ts(&s)), + revoked_at: r.get::<_, Option>(6)?.map(|s| parse_ts(&s)), + }) + } + + const PEER_COLS: &'static str = + "id, name, daemon_id, url, created_at, last_seen_at, revoked_at"; + + pub fn create_peer(&self, name: &str, url: Option<&str>) -> anyhow::Result { + let peer = Peer { + id: new_id(), + name: name.to_string(), + daemon_id: None, + url: url.map(str::to_string), + created_at: now(), + last_seen_at: None, + revoked_at: None, + }; + self.lock().execute( + "INSERT INTO peer(id, name, url, created_at) VALUES (?1, ?2, ?3, ?4)", + params![peer.id, peer.name, peer.url, ts(peer.created_at)], + )?; + Ok(peer) + } + + pub fn list_peers(&self) -> anyhow::Result> { + let conn = self.lock(); + let mut stmt = conn.prepare(&format!( + "SELECT {} FROM peer ORDER BY name", + Self::PEER_COLS + ))?; + let rows = stmt.query_map([], Self::peer_from_row)?; + Ok(rows.collect::>()?) + } + + pub fn get_peer(&self, peer_id: &str) -> anyhow::Result> { + let conn = self.lock(); + Ok(conn + .query_row( + &format!("SELECT {} FROM peer WHERE id = ?1", Self::PEER_COLS), + params![peer_id], + Self::peer_from_row, + ) + .optional()?) + } + + pub fn get_peer_by_name(&self, name: &str) -> anyhow::Result> { + let conn = self.lock(); + Ok(conn + .query_row( + &format!( + "SELECT {} FROM peer WHERE name = ?1 COLLATE NOCASE", + Self::PEER_COLS + ), + params![name], + Self::peer_from_row, + ) + .optional()?) + } + + /// Binds the peer to the daemon that presented its token. The first + /// daemon wins; any other daemon presenting the same token is refused. + pub fn bind_peer_daemon(&self, peer_id: &str, daemon_id: &str) -> anyhow::Result { + let conn = self.lock(); + conn.execute( + "UPDATE peer SET daemon_id = ?2 WHERE id = ?1 AND daemon_id IS NULL", + params![peer_id, daemon_id], + )?; + let bound: Option = conn.query_row( + "SELECT daemon_id FROM peer WHERE id = ?1", + params![peer_id], + |r| r.get(0), + )?; + Ok(bound.as_deref() == Some(daemon_id)) + } + + pub fn touch_peer(&self, peer_id: &str) -> anyhow::Result<()> { + self.lock().execute( + "UPDATE peer SET last_seen_at = ?2 WHERE id = ?1", + params![peer_id, ts(now())], + )?; + Ok(()) + } + + pub fn revoke_peer(&self, peer_id: &str) -> anyhow::Result { + let changed = self.lock().execute( + "UPDATE peer SET revoked_at = ?2 WHERE id = ?1 AND revoked_at IS NULL", + params![peer_id, ts(now())], + )?; + Ok(changed > 0) + } + + // ---- links ---- + + /// Exposes a local bot to a peer: the peer may now deliver to it. + pub fn expose_bot_to_peer(&self, peer_id: &str, bot_id: &str) -> anyhow::Result<()> { + self.lock().execute( + "INSERT OR IGNORE INTO peer_link(peer_id, bot_id) VALUES (?1, ?2)", + params![peer_id, bot_id], + )?; + Ok(()) + } + + pub fn is_exposed_to_peer(&self, peer_id: &str, bot_id: &str) -> anyhow::Result { + let conn = self.lock(); + let found: Option = conn + .query_row( + "SELECT 1 FROM peer_link WHERE peer_id = ?1 AND bot_id = ?2", + params![peer_id, bot_id], + |r| r.get(0), + ) + .optional()?; + Ok(found.is_some()) + } + + /// The live linked bot standing in for `remote_bot_id` in a project. + pub fn linked_bot( + &self, + peer_id: &str, + remote_bot_id: &str, + project_id: &str, + ) -> anyhow::Result> { + let conn = self.lock(); + let sql = format!( + "SELECT {} FROM bot WHERE peer_id = ?1 AND remote_bot_id = ?2 AND project_id = ?3 \ + AND deleted_at IS NULL", + Self::BOT_COLS + ); + Ok(conn + .query_row( + &sql, + params![peer_id, remote_bot_id, project_id], + Self::bot_from_row, + ) + .optional()?) + } + + /// A bot row standing in for a bot that runs on `peer_id`. It gets a DM + /// conversation like any bot, an empty workspace path, and a directory + /// name no local bot can have, so nothing ever provisions files for it. + pub fn create_linked_bot( + &self, + project_id: &str, + remote: &RemoteBot, + peer_id: &str, + ) -> anyhow::Result { + let dir_name = format!(".peer-{}", &new_id()[..8]); + let mut bot = self.create_bot_with_runtime( + project_id, + &remote.name, + &remote.description, + "", + &remote.avatar, + "", + &dir_name, + None, + remote.runtime, + )?; + self.lock().execute( + "UPDATE bot SET peer_id = ?2, remote_bot_id = ?3 WHERE id = ?1", + params![bot.id, peer_id, remote.id], + )?; + bot.peer_id = Some(peer_id.to_string()); + bot.remote_bot_id = Some(remote.id.clone()); + Ok(bot) + } + + // ---- id mappings ---- + + pub fn map_peer_message( + &self, + peer_id: &str, + remote_message_id: &str, + message_id: &str, + ) -> anyhow::Result<()> { + self.lock().execute( + "INSERT OR IGNORE INTO peer_message(peer_id, remote_message_id, message_id) + VALUES (?1, ?2, ?3)", + params![peer_id, remote_message_id, message_id], + )?; + Ok(()) + } + + /// The local copy of a message the peer knows as `remote_message_id`. + pub fn local_message_for( + &self, + peer_id: &str, + remote_message_id: &str, + ) -> anyhow::Result> { + self.peer_lookup( + "SELECT message_id FROM peer_message WHERE peer_id = ?1 AND remote_message_id = ?2", + peer_id, + remote_message_id, + ) + } + + /// The peer's id for a local message, when the message crossed the link. + pub fn remote_message_for( + &self, + peer_id: &str, + message_id: &str, + ) -> anyhow::Result> { + self.peer_lookup( + "SELECT remote_message_id FROM peer_message WHERE peer_id = ?1 AND message_id = ?2", + peer_id, + message_id, + ) + } + + pub fn map_peer_task( + &self, + peer_id: &str, + remote_task_id: &str, + task_id: &str, + ) -> anyhow::Result<()> { + self.lock().execute( + "INSERT OR IGNORE INTO peer_task(peer_id, remote_task_id, task_id) + VALUES (?1, ?2, ?3)", + params![peer_id, remote_task_id, task_id], + )?; + Ok(()) + } + + /// The peer's id for the other half of a mirrored task. + pub fn remote_task_for(&self, peer_id: &str, task_id: &str) -> anyhow::Result> { + self.peer_lookup( + "SELECT remote_task_id FROM peer_task WHERE peer_id = ?1 AND task_id = ?2", + peer_id, + task_id, + ) + } + + fn peer_lookup(&self, sql: &str, peer_id: &str, key: &str) -> anyhow::Result> { + let conn = self.lock(); + Ok(conn + .query_row(sql, params![peer_id, key], |r| r.get(0)) + .optional()?) + } +} + +impl Db { + /// Every task a message opened, whatever its state. A result or a cancel + /// crossing the link has to find the task it closes after the close. + pub fn tasks_with_origin(&self, origin_message_id: &str) -> anyhow::Result> { + let ids: Vec = { + let conn = self.lock(); + let mut stmt = conn.prepare("SELECT id FROM task WHERE origin_message_id = ?1")?; + let rows = stmt.query_map(params![origin_message_id], |r| r.get(0))?; + rows.collect::>()? + }; + let mut tasks = Vec::with_capacity(ids.len()); + for id in ids { + if let Some(task) = self.get_task(&id)? { + tasks.push(task); + } + } + Ok(tasks) + } +} diff --git a/crates/gravityd/src/delivery.rs b/crates/gravityd/src/delivery.rs index 90384d4..08491e1 100644 --- a/crates/gravityd/src/delivery.rs +++ b/crates/gravityd/src/delivery.rs @@ -3,10 +3,13 @@ //! consumers deduplicate by delivery id / idempotency key. A bot that is not //! ready (stopped, socket unknown) defers without consuming retry attempts. +use std::sync::Arc; + use bus::envelope::{render_message, render_routine}; -use bus::DeliveryState; +use bus::{DeliveryState, Message}; use chrono::Duration as ChronoDuration; +use crate::app::AppState; use crate::config::DeliveryConfig; use crate::db::Db; use crate::events::{Events, Push}; @@ -18,6 +21,8 @@ use crate::supervisor::Supervisor; const MAX_NOT_READY_BACKOFF_SECONDS: i64 = 60; pub struct DeliveryWorker { + /// For forwarding to linked bots, which runs through the peer link. + pub app: Arc, pub db: Db, pub supervisor: Supervisor, pub events: Events, @@ -31,22 +36,22 @@ impl DeliveryWorker { tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); loop { tick.tick().await; - if let Err(e) = self.step() { + if let Err(e) = self.step().await { tracing::warn!(error = %e, "delivery worker step failed"); } } } - pub fn step(&self) -> anyhow::Result<()> { + pub async fn step(&self) -> anyhow::Result<()> { self.db.recover_expired_leases()?; let due = self.db.lease_due_deliveries(self.cfg.lease_seconds, 20)?; for delivery in due { - self.attempt(&delivery.id)?; + self.attempt(&delivery.id).await?; } Ok(()) } - fn attempt(&self, delivery_id: &str) -> anyhow::Result<()> { + async fn attempt(&self, delivery_id: &str) -> anyhow::Result<()> { let Some(delivery) = self.db.get_delivery(delivery_id)? else { return Ok(()); }; @@ -55,27 +60,26 @@ impl DeliveryWorker { .mark_delivery_retry(delivery_id, "message missing", chrono::Utc::now(), 0)?; return Ok(()); }; - // A decision notice was already rendered by the registry, which owns - // its header and its phase. Wrapping it in `[msg #N …]` would bury the - // one thing that makes it authority: that it comes from the owner. - let text = if msg.decision_id.is_some() { - msg.body.clone() - } else if msg.sender.kind == bus::SenderKind::Routine { - let run_id = self.db.run_id_for_delivery(delivery_id)?; - render_routine(&msg.sender.name, msg.num, run_id.as_deref(), &msg.body) - } else { - let ref_num = match &msg.ref_message_id { - Some(rid) => self.db.get_message(rid)?.map(|m| m.num), - None => None, - }; - let task_id = self - .db - .open_task_for_message(&msg.id, &delivery.bot_id)? - .map(|t| t.id); - render_message(&msg, ref_num, task_id.as_deref()) + let outcome = match self.db.get_bot(&delivery.bot_id)? { + // A bot that runs on a peer: the peer stores and delivers it. + Some(target) if target.is_linked() => { + match crate::peer::forward(&self.app, &target, &msg).await { + Ok(()) => Ok(()), + Err(crate::peer::ForwardError::Offline) => Err( + crate::supervisor::DeliverError::NotReady("peer offline".to_string()), + ), + Err(crate::peer::ForwardError::Failed(e)) => { + Err(crate::supervisor::DeliverError::Failed(e)) + } + } + } + _ => { + let text = self.render(delivery_id, &delivery.bot_id, &msg)?; + self.supervisor.deliver(&delivery.bot_id, &text) + } }; - match self.supervisor.deliver(&delivery.bot_id, &text) { + match outcome { Ok(()) => { self.db.mark_delivered(delivery_id)?; tracing::info!(delivery_id, bot_id = %delivery.bot_id, message_id = %msg.id, "delivered"); @@ -120,4 +124,42 @@ impl DeliveryWorker { } Ok(()) } + + /// The envelope a local session reads for `msg`. + fn render(&self, delivery_id: &str, bot_id: &str, msg: &Message) -> anyhow::Result { + // A decision notice was already rendered by the registry, which owns + // its header and its phase. Wrapping it in `[msg #N …]` would bury the + // one thing that makes it authority: that it comes from the owner. + Ok(if msg.decision_id.is_some() { + msg.body.clone() + } else if msg.sender.kind == bus::SenderKind::Routine { + let run_id = self.db.run_id_for_delivery(delivery_id)?; + render_routine(&msg.sender.name, msg.num, run_id.as_deref(), &msg.body) + } else { + let ref_num = match &msg.ref_message_id { + Some(rid) => self.db.get_message(rid)?.map(|m| m.num), + None => None, + }; + let task_id = self + .db + .open_task_for_message(&msg.id, bot_id)? + .map(|t| t.id); + render_message(&self.sender_view(msg)?, ref_num, task_id.as_deref()) + }) + } + + /// A linked sender is named with its machine, so a bot can tell a peer + /// on the other daemon from a colleague here. + fn sender_view(&self, msg: &Message) -> anyhow::Result { + let mut view = msg.clone(); + let Some(bot_id) = &msg.sender.bot_id else { + return Ok(view); + }; + if let Some(peer_id) = self.db.get_bot(bot_id)?.and_then(|b| b.peer_id) { + if let Some(peer) = self.db.get_peer(&peer_id)? { + view.sender.name = format!("{} @ {}", msg.sender.name, peer.name); + } + } + Ok(view) + } } diff --git a/crates/gravityd/src/lib.rs b/crates/gravityd/src/lib.rs index 1087461..d6b2268 100644 --- a/crates/gravityd/src/lib.rs +++ b/crates/gravityd/src/lib.rs @@ -18,6 +18,7 @@ pub mod messaging; pub mod model; pub mod overrides; pub mod paths; +pub mod peer; mod permissions; pub mod projectmgmt; pub mod routine_validation; diff --git a/crates/gravityd/src/main.rs b/crates/gravityd/src/main.rs index ece2371..9f9c03d 100644 --- a/crates/gravityd/src/main.rs +++ b/crates/gravityd/src/main.rs @@ -62,6 +62,10 @@ async fn main() -> anyhow::Result<()> { println!("restored into {}", cfg.home.display()); return Ok(()); } + Some("peer") => { + gravityd::peer::cli::run(&cfg, &args[1..]).await?; + return Ok(()); + } Some("service") => { let paths = gravityd::service::ServicePaths::new(cfg.home.clone(), cfg.user_home.clone()); diff --git a/crates/gravityd/src/mcp/tools.rs b/crates/gravityd/src/mcp/tools.rs index 8164aa3..31e6512 100644 --- a/crates/gravityd/src/mcp/tools.rs +++ b/crates/gravityd/src/mcp/tools.rs @@ -204,7 +204,7 @@ pub(super) fn list_bots(app: &Arc, bot_id: &str) -> anyhow::Result, bot_id: &str) -> anyhow::Result) -> String { across both ends; `note` is an FYI that expects no answer. When a\n\ send is refused, the error says what to do instead — do it rather\n\ than retrying.\n\n\ + Some colleagues run on another of the owner's machines: `list_bots`\n\ + shows them with a `machine`, and their messages arrive as\n\ + `from NAME @ MACHINE`. Message them like anyone else. Their disk is\n\ + not yours: list files you hand them in `complete_task`'s\n\ + `artifacts`, which copies them across, and expect a path they mention\n\ + elsewhere to be unreadable here.\n\n\ ## Messages carry authority\n\n\ Every message on the bus is authenticated by the daemon: the name in\n\ the header is who sent it. Bots in your project are colleagues, not\n\ diff --git a/crates/gravityd/src/peer.rs b/crates/gravityd/src/peer.rs new file mode 100644 index 0000000..aab246c --- /dev/null +++ b/crates/gravityd/src/peer.rs @@ -0,0 +1,19 @@ +//! Peer daemons: bots on two of the owner's machines working as one team. +//! +//! A bot that runs on a peer stands in here as a linked bot. Sending to it is +//! sending to any bot; the delivery worker forwards the message over the peer +//! link instead of writing it to a local session, and the peer stores it as if +//! the sender were one of its own. See `docs/peer-bots.md`. + +mod artifacts; +pub mod cli; +mod forward; +mod frames; +mod hub; +mod inbound; +mod receive; +mod socket; + +pub use forward::{forward, ForwardError}; +pub use hub::{PeerError, PeerHub}; +pub use socket::{peer_handler, spawn_dialer, spawn_dialers}; diff --git a/crates/gravityd/src/peer/artifacts.rs b/crates/gravityd/src/peer/artifacts.rs new file mode 100644 index 0000000..31e3006 --- /dev/null +++ b/crates/gravityd/src/peer/artifacts.rs @@ -0,0 +1,183 @@ +//! Files that travel with a result. `complete_task` lists artifact paths in +//! the `done` body; a path on the other machine is useless to the requester, +//! so the files themselves cross the link and the listing is rewritten to +//! where they landed. + +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use base64::Engine; +use bus::{Bot, Peer, MAX_PEER_ARTIFACTS_TOTAL_BYTES, MAX_PEER_ARTIFACT_BYTES}; + +use crate::app::AppState; + +use super::frames::{ArtifactFrame, MessageFrame}; + +/// The separator `complete_task` writes between a result and its listing. +const LISTING: &str = "\n\nartifacts:"; + +/// Splits a result body into its text and the artifact paths it lists. +pub(super) fn split_listing(body: &str) -> (&str, Vec<&str>) { + match body.rsplit_once(LISTING) { + Some((result, listing)) => { + let paths = listing + .lines() + .filter_map(|line| line.strip_prefix("- ")) + .map(str::trim) + .filter(|p| !p.is_empty()) + .collect(); + (result, paths) + } + None => (body, Vec::new()), + } +} + +/// Reads the files a result lists, for sending with it. Only files inside the +/// sending bot's own directory or its project's artifacts directory are read, +/// so a bot cannot exfiltrate anything else by listing it. +pub(super) fn collect(app: &Arc, sender: &Bot, body: &str) -> Vec { + let (_, paths) = split_listing(body); + let roots = allowed_roots(app, sender); + let workspace = PathBuf::from(&sender.workspace_path); + let mut total = 0u64; + paths + .into_iter() + .map(|listed| { + let skipped = |reason: &str| ArtifactFrame { + path: listed.to_string(), + bytes: None, + skipped: Some(reason.to_string()), + }; + let path = workspace.join(listed); + let Ok(path) = path.canonicalize() else { + return skipped("file not found"); + }; + if !roots.iter().any(|root| path.starts_with(root)) { + return skipped("outside the bot's workspace and the project artifacts"); + } + let size = match path.metadata() { + Ok(meta) if meta.is_file() => meta.len(), + _ => return skipped("not a regular file"), + }; + if size > MAX_PEER_ARTIFACT_BYTES { + return skipped("larger than the 16 MiB transfer limit"); + } + if total + size > MAX_PEER_ARTIFACTS_TOTAL_BYTES { + return skipped("over the 48 MiB limit for one result"); + } + match std::fs::read(&path) { + Ok(bytes) => { + total += size; + ArtifactFrame { + path: listed.to_string(), + bytes: Some(base64::engine::general_purpose::STANDARD.encode(bytes)), + skipped: None, + } + } + Err(_) => skipped("could not be read"), + } + }) + .collect() +} + +fn allowed_roots(app: &Arc, sender: &Bot) -> Vec { + let mut roots = Vec::new(); + if let Some(bot_root) = Path::new(&sender.workspace_path).parent() { + roots.extend(bot_root.canonicalize().ok()); + } + if let Ok(Some(project)) = app.db.get_project(&sender.project_id) { + let artifacts = crate::paths::artifacts_dir(&app.cfg, &project.dir_name); + roots.extend(artifacts.canonicalize().ok()); + } + roots +} + +/// Writes a forwarded result's files into the recipient project's artifacts +/// directory and returns the body with its listing pointing at them. +pub(super) fn store( + app: &Arc, + peer: &Peer, + to: &Bot, + frame: &MessageFrame, +) -> anyhow::Result { + let project = app + .db + .get_project(&to.project_id)? + .ok_or_else(|| anyhow::anyhow!("project missing"))?; + let short: String = frame.id.chars().take(8).collect(); + let dir = crate::paths::artifacts_dir(&app.cfg, &project.dir_name) + .join("peers") + .join(bus::names::dir_name(&peer.name)) + .join(short); + let (result, _) = split_listing(&frame.body); + let mut listing = String::new(); + for artifact in &frame.artifacts { + let line = match (&artifact.bytes, &artifact.skipped) { + (Some(encoded), _) => { + let bytes = base64::engine::general_purpose::STANDARD.decode(encoded)?; + std::fs::create_dir_all(&dir)?; + let target = unique_path(&dir, &file_name(&artifact.path)); + std::fs::write(&target, bytes)?; + target.display().to_string() + } + (None, reason) => format!( + "{} (on {}; not transferred: {})", + artifact.path, + peer.name, + reason.as_deref().unwrap_or("unknown reason") + ), + }; + listing.push_str(&format!("\n- {line}")); + } + Ok(format!("{result}{LISTING}{listing}")) +} + +/// The last component of a path written on either platform. +fn file_name(path: &str) -> String { + let base = path.rsplit(['/', '\\']).next().unwrap_or(path).trim(); + if base.is_empty() || base == "." || base == ".." { + "artifact".to_string() + } else { + base.to_string() + } +} + +/// `dir/name`, or `dir/name-2` and so on when two artifacts share a name. +fn unique_path(dir: &Path, name: &str) -> PathBuf { + let first = dir.join(name); + if !first.exists() { + return first; + } + let (stem, ext) = match name.rsplit_once('.') { + Some((stem, ext)) if !stem.is_empty() => (stem, format!(".{ext}")), + _ => (name, String::new()), + }; + (2..) + .map(|n| dir.join(format!("{stem}-{n}{ext}"))) + .find(|candidate| !candidate.exists()) + .unwrap_or(first) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn splits_the_listing_complete_task_writes() { + let (result, paths) = split_listing("built it\n\nartifacts:\n- out/app.exe\n- log.txt"); + assert_eq!(result, "built it"); + assert_eq!(paths, vec!["out/app.exe", "log.txt"]); + } + + #[test] + fn a_result_without_a_listing_has_no_paths() { + assert_eq!(split_listing("done"), ("done", Vec::new())); + } + + #[test] + fn takes_the_file_name_from_windows_and_unix_paths() { + assert_eq!(file_name(r"C:\work\out\app.exe"), "app.exe"); + assert_eq!(file_name("/tmp/build/log.txt"), "log.txt"); + assert_eq!(file_name("../.."), "artifact"); + } +} diff --git a/crates/gravityd/src/peer/cli.rs b/crates/gravityd/src/peer/cli.rs new file mode 100644 index 0000000..2437d65 --- /dev/null +++ b/crates/gravityd/src/peer/cli.rs @@ -0,0 +1,197 @@ +//! `gravityd peer …`: pairing and linking from a terminal. The commands talk +//! to the running daemon over its control plane with the owner token, so they +//! work while it serves, and they are exactly what the app will send. + +use futures::{SinkExt, StreamExt}; +use serde_json::{json, Value}; +use tokio_tungstenite::tungstenite::Message; + +use crate::config::Config; + +const USAGE: &str = "usage: + gravityd peer invite [--url ws://host:port/peer] + gravityd peer add + gravityd peer list + gravityd peer bots + gravityd peer link --project + gravityd peer revoke "; + +pub async fn run(cfg: &Config, args: &[String]) -> anyhow::Result<()> { + let arg = |i: usize| args.get(i).map(String::as_str); + let flag = |name: &str| { + args.iter() + .position(|a| a == name) + .and_then(|i| args.get(i + 1)) + .map(String::as_str) + }; + let mut daemon = Daemon::connect(cfg).await?; + match (arg(0), arg(1), arg(2)) { + (Some("invite"), Some(name), _) => { + let mut req = json!({ "type": "create_peer_invite", "name": name }); + if let Some(url) = flag("--url") { + req["url"] = json!(url); + } + let reply = daemon.request(req).await?; + println!("Invite for {name} (shown once):\n"); + println!("{}", reply["invite"].as_str().unwrap_or_default()); + println!("\nOn {name}, run: gravityd peer add "); + } + (Some("add"), Some(name), Some(invite)) => { + daemon + .request(json!({ "type": "add_peer", "name": name, "invite": invite })) + .await?; + println!("Added {name}. This daemon now keeps a link open to it."); + } + (Some("list"), _, _) => { + for peer in daemon.peers().await? { + let state = if peer["revoked_at"].is_string() { + "revoked" + } else if peer["online"].as_bool() == Some(true) { + "online" + } else { + "offline" + }; + let dials = peer["url"].as_str().map(|u| format!(" dials {u}")); + println!( + "{} {state}{}", + peer["name"].as_str().unwrap_or_default(), + dials.unwrap_or_default() + ); + } + } + (Some("bots"), Some(peer), _) => { + let peer = daemon.peer(peer).await?; + for bot in daemon.peer_bots(&peer).await? { + println!( + "{} ({}) {}", + bot["name"].as_str().unwrap_or_default(), + bot["project"].as_str().unwrap_or_default(), + bot["description"].as_str().unwrap_or_default() + ); + } + } + (Some("link"), Some(peer), Some(bot)) => { + let project_name = flag("--project") + .ok_or_else(|| anyhow::anyhow!("link needs --project "))?; + let peer = daemon.peer(peer).await?; + let remote = daemon + .peer_bots(&peer) + .await? + .into_iter() + .find(|b| { + b["name"] + .as_str() + .is_some_and(|n| n.eq_ignore_ascii_case(bot)) + }) + .ok_or_else(|| anyhow::anyhow!("no bot named '{bot}' on that peer"))?; + let projects = daemon.request(json!({ "type": "list_projects" })).await?; + let project = projects["projects"] + .as_array() + .into_iter() + .flatten() + .find(|p| { + p["name"] + .as_str() + .is_some_and(|n| n.eq_ignore_ascii_case(project_name)) + }) + .ok_or_else(|| anyhow::anyhow!("no project named '{project_name}'"))?; + daemon + .request(json!({ + "type": "link_peer_bot", + "peer_id": peer, + "remote_bot_id": remote["id"], + "project_id": project["id"], + })) + .await?; + println!("Linked {bot} into {project_name}. Bots there can message it by name."); + } + (Some("revoke"), Some(peer), _) => { + let peer = daemon.peer(peer).await?; + daemon + .request(json!({ "type": "revoke_peer", "peer_id": peer })) + .await?; + println!("Revoked. Its token no longer works on this daemon."); + } + _ => anyhow::bail!("{USAGE}"), + } + Ok(()) +} + +type Socket = + tokio_tungstenite::WebSocketStream>; + +struct Daemon { + socket: Socket, + next: u64, +} + +impl Daemon { + async fn connect(cfg: &Config) -> anyhow::Result { + let port = crate::home::runtime_port(&cfg.home).unwrap_or(cfg.port); + let token = std::fs::read_to_string(cfg.secrets_dir().join("client.token")) + .map_err(|e| anyhow::anyhow!("cannot read the owner token: {e}"))?; + let url = format!("ws://127.0.0.1:{port}/ws"); + let (socket, _) = tokio_tungstenite::connect_async(url.as_str()) + .await + .map_err(|e| anyhow::anyhow!("gravityd is not answering on {url}: {e}"))?; + let mut daemon = Self { socket, next: 1 }; + daemon + .request(json!({ + "type": "hello", + "protocol_version": crate::app::PROTOCOL_VERSION, + "token": token.trim(), + "client": "gravityd-cli" + })) + .await?; + Ok(daemon) + } + + /// Sends a request and returns its reply, skipping pushes in between. + async fn request(&mut self, mut req: Value) -> anyhow::Result { + let req_id = self.next; + self.next += 1; + req["req_id"] = json!(req_id); + self.socket.send(Message::Text(req.to_string())).await?; + while let Some(frame) = self.socket.next().await { + let Message::Text(text) = frame? else { + continue; + }; + let reply: Value = serde_json::from_str(&text)?; + if reply["req_id"].as_u64() != Some(req_id) { + continue; + } + if reply["type"] == "error" { + anyhow::bail!("{}", reply["message"].as_str().unwrap_or("request failed")); + } + return Ok(reply); + } + anyhow::bail!("gravityd closed the connection") + } + + async fn peers(&mut self) -> anyhow::Result> { + let reply = self.request(json!({ "type": "list_peers" })).await?; + Ok(reply["peers"].as_array().cloned().unwrap_or_default()) + } + + /// The id of the live peer with this name. + async fn peer(&mut self, name: &str) -> anyhow::Result { + self.peers() + .await? + .into_iter() + .find(|p| { + p["name"] + .as_str() + .is_some_and(|n| n.eq_ignore_ascii_case(name)) + && !p["revoked_at"].is_string() + }) + .and_then(|p| p["id"].as_str().map(str::to_string)) + .ok_or_else(|| anyhow::anyhow!("no peer named '{name}'")) + } + + async fn peer_bots(&mut self, peer_id: &str) -> anyhow::Result> { + let reply = self + .request(json!({ "type": "list_peer_bots", "peer_id": peer_id })) + .await?; + Ok(reply["bots"].as_array().cloned().unwrap_or_default()) + } +} diff --git a/crates/gravityd/src/peer/forward.rs b/crates/gravityd/src/peer/forward.rs new file mode 100644 index 0000000..7011918 --- /dev/null +++ b/crates/gravityd/src/peer/forward.rs @@ -0,0 +1,188 @@ +//! Forwarding a delivery whose recipient is a linked bot: the delivery +//! worker's branch for bots that run on a peer. + +use std::sync::Arc; + +use bus::{Bot, Message, MessageKind, Peer, SenderKind, TaskState}; +use serde_json::json; + +use crate::app::AppState; + +use super::frames::{ClosesFrame, FromFrame, MessageFrame, Received, TaskFrame}; +use super::hub::PeerError; + +pub enum ForwardError { + /// The peer is not connected. Nothing was sent; try again later. + Offline, + Failed(anyhow::Error), +} + +/// Hands `msg` to the peer `target` runs on. `Ok` once the peer has stored it, +/// or once it is clear the message is not the peer's to see. +pub async fn forward(app: &Arc, target: &Bot, msg: &Message) -> Result<(), ForwardError> { + let fail = |e: anyhow::Error| ForwardError::Failed(e); + let peer_id = target + .peer_id + .clone() + .ok_or_else(|| fail(anyhow::anyhow!("not a linked bot")))?; + let peer = app + .db + .get_peer(&peer_id) + .map_err(fail)? + .filter(|p| p.revoked_at.is_none()) + .ok_or_else(|| fail(anyhow::anyhow!("peer revoked")))?; + if !app.peers.is_online(&peer.id) { + return Err(ForwardError::Offline); + } + let frame = { + let app = app.clone(); + let (peer, target, msg) = (peer.clone(), target.clone(), msg.clone()); + tokio::task::spawn_blocking(move || build(&app, &peer, &target, &msg)) + .await + .map_err(|e| fail(e.into()))? + .map_err(fail)? + }; + let Some(frame) = frame else { + return Ok(()); + }; + let task_id = frame.task.as_ref().map(|t| t.id.clone()); + let request = json!({ "type": "message", "message": frame }); + let result = match app.peers.request(&peer.id, request).await { + Ok(result) => result, + Err(PeerError::Offline) => return Err(ForwardError::Offline), + Err(PeerError::Rejected(reason)) => { + return Err(fail(anyhow::anyhow!("{} refused it: {reason}", peer.name))) + } + }; + let received: Received = serde_json::from_value(result).map_err(|e| fail(e.into()))?; + app.db + .map_peer_message(&peer.id, &received.message_id, &msg.id) + .map_err(fail)?; + if let (Some(local), Some(remote)) = (task_id, received.task_id) { + app.db + .map_peer_task(&peer.id, &remote, &local) + .map_err(fail)?; + } + Ok(()) +} + +/// The frame for `msg`, or `None` when it stays on this daemon. +fn build( + app: &Arc, + peer: &Peer, + target: &Bot, + msg: &Message, +) -> anyhow::Result> { + let remote_bot_id = target + .remote_bot_id + .clone() + .ok_or_else(|| anyhow::anyhow!("linked bot has no remote id"))?; + let mut sender_bot = None; + let from = match msg.sender.kind { + // The daemon's own notices (expiries, renames, introductions) describe + // this daemon's view. The peer produces its own for its side. + SenderKind::User if msg.sender.name == "system" => return Ok(None), + SenderKind::User => FromFrame::User, + SenderKind::Bot => { + let id = msg.sender.bot_id.as_deref().unwrap_or_default(); + let bot = app + .db + .get_bot(id)? + .ok_or_else(|| anyhow::anyhow!("sender {id} missing"))?; + anyhow::ensure!( + !bot.is_linked(), + "{} runs on another peer; messages are not relayed between peers", + bot.name + ); + // The recipient's answers come back addressed to the sender, so + // the sender has to accept deliveries from this peer. + app.db.expose_bot_to_peer(&peer.id, &bot.id)?; + let view = super::inbound::remote_view(&bot, String::new()); + sender_bot = Some(bot); + FromFrame::Bot { bot: view } + } + SenderKind::Routine => anyhow::bail!("routine prompts are never forwarded"), + }; + + let ref_message_id = match &msg.ref_message_id { + Some(r) => Some( + app.db + .remote_message_for(&peer.id, r)? + .unwrap_or_else(|| r.clone()), + ), + None => None, + }; + + let task = match msg.kind { + MessageKind::Task => { + let task = app + .db + .open_task_for_message(&msg.id, &target.id)? + .ok_or_else(|| anyhow::anyhow!("task for message {} is no longer open", msg.num))?; + Some(TaskFrame { + id: task.id, + deadline_at: task.deadline_at, + hop_count: task.hop_count, + }) + } + _ => None, + }; + + let closes_task = closes(app, peer, target, msg)?; + let artifacts = match (&sender_bot, msg.kind) { + (Some(bot), MessageKind::Done) => super::artifacts::collect(app, bot, &msg.body), + _ => Vec::new(), + }; + + Ok(Some(MessageFrame { + id: msg.id.clone(), + to_bot_id: remote_bot_id, + from, + kind: msg.kind, + body: msg.body.clone(), + ref_message_id, + task, + closes_task, + artifacts, + })) +} + +/// The peer's half of the task this message closes: a result for a task the +/// linked bot delegated here, or the cancellation of one delegated to it. +fn closes( + app: &Arc, + peer: &Peer, + target: &Bot, + msg: &Message, +) -> anyhow::Result> { + let Some(origin) = &msg.ref_message_id else { + return Ok(None); + }; + let tasks = app.db.tasks_with_origin(origin)?; + let found = match msg.kind { + MessageKind::Done => tasks + .iter() + .find(|t| t.from_bot_id.as_deref() == Some(target.id.as_str())) + .map(|t| (t, TaskState::Done)), + MessageKind::Note => tasks + .iter() + .find(|t| t.to_bot_id == target.id && t.state == TaskState::Cancelled) + .map(|t| (t, TaskState::Cancelled)), + _ => None, + }; + let Some((task, state)) = found else { + anyhow::ensure!( + msg.kind != MessageKind::Done, + "result for a task {} did not delegate", + target.name + ); + return Ok(None); + }; + match app.db.remote_task_for(&peer.id, &task.id)? { + Some(remote) => Ok(Some(ClosesFrame { id: remote, state })), + // A task cancelled before it ever reached the peer has nothing there + // to close; the note still tells the bot to stop. + None if state == TaskState::Cancelled => Ok(None), + None => anyhow::bail!("task {} has no counterpart on {}", task.id, peer.name), + } +} diff --git a/crates/gravityd/src/peer/frames.rs b/crates/gravityd/src/peer/frames.rs new file mode 100644 index 0000000..2843a79 --- /dev/null +++ b/crates/gravityd/src/peer/frames.rs @@ -0,0 +1,74 @@ +//! Frames exchanged on a peer link. Ids in a frame are the sender's own, +//! except `to_bot_id` and `closes_task.id`, which the sender has already +//! translated into the receiver's ids. + +use bus::{MessageKind, RemoteBot, TaskState}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +/// One message crossing the link, with whatever it opens or closes. +#[derive(Debug, Serialize, Deserialize)] +pub struct MessageFrame { + /// The message's id on the sending daemon. The receiver dedupes on it. + pub id: String, + pub to_bot_id: String, + pub from: FromFrame, + pub kind: MessageKind, + pub body: String, + /// The message this answers, in whichever id the receiver knows it by. + #[serde(rename = "ref", default, skip_serializing_if = "Option::is_none")] + pub ref_message_id: Option, + /// Set when the message opens a task: the receiver mirrors it. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub task: Option, + /// Set when the message is the result or the cancellation of a task the + /// receiver holds. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub closes_task: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub artifacts: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum FromFrame { + /// The owner, writing from the other machine's app. + User, + Bot { + bot: RemoteBot, + }, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct TaskFrame { + pub id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub deadline_at: Option>, + pub hop_count: i64, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ClosesFrame { + pub id: String, + pub state: TaskState, +} + +/// A file attached to a result. `bytes` is base64; it is absent when the file +/// could not be sent, and `skipped` says why. +#[derive(Debug, Serialize, Deserialize)] +pub struct ArtifactFrame { + /// The path as the sender's result listed it. + pub path: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub bytes: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub skipped: Option, +} + +/// What the receiver answers a message frame with. +#[derive(Debug, Serialize, Deserialize)] +pub struct Received { + pub message_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub task_id: Option, +} diff --git a/crates/gravityd/src/peer/hub.rs b/crates/gravityd/src/peer/hub.rs new file mode 100644 index 0000000..bd43f28 --- /dev/null +++ b/crates/gravityd/src/peer/hub.rs @@ -0,0 +1,182 @@ +//! Live peer links. Each paired daemon has at most one connection, whichever +//! side dialed it; requests go out over it and wait for the matching response. + +use std::collections::HashMap; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use serde_json::{json, Value}; +use tokio::sync::{mpsc, oneshot, Notify}; + +use crate::app::AppState; + +/// How long a request waits for the peer's response. A forwarded message with +/// artifacts is the slowest frame there is, and a tailnet hop is fast. +const REQUEST_TIMEOUT: Duration = Duration::from_secs(60); + +/// Why a request to a peer did not produce a result. +#[derive(Debug)] +pub enum PeerError { + /// No live link. Nothing was sent, so retrying later is safe. + Offline, + /// The peer answered with a refusal, or never answered. + Rejected(String), +} + +impl std::fmt::Display for PeerError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PeerError::Offline => write!(f, "peer is offline"), + PeerError::Rejected(reason) => write!(f, "{reason}"), + } + } +} + +type Pending = Arc>>>; + +struct Link { + out: mpsc::UnboundedSender, + pending: Pending, + /// Distinguishes this link from a newer one for the same peer, so the old + /// one closing does not unregister its replacement. + generation: u64, + closed: Arc, +} + +#[derive(Clone, Default)] +pub struct PeerHub { + links: Arc>>, + counter: Arc, +} + +impl PeerHub { + fn lock(&self) -> std::sync::MutexGuard<'_, HashMap> { + self.links.lock().unwrap_or_else(|e| e.into_inner()) + } + + pub fn is_online(&self, peer_id: &str) -> bool { + self.lock().contains_key(peer_id) + } + + /// Drops the peer's link, if any. Used when the peer is revoked. + pub fn disconnect(&self, peer_id: &str) { + if let Some(link) = self.lock().remove(peer_id) { + link.closed.notify_one(); + } + } + + /// Sends a request and waits for the peer's result. + pub async fn request(&self, peer_id: &str, mut frame: Value) -> Result { + let (out, pending) = { + let links = self.lock(); + let link = links.get(peer_id).ok_or(PeerError::Offline)?; + (link.out.clone(), link.pending.clone()) + }; + let req_id = self.counter.fetch_add(1, Ordering::Relaxed); + let (tx, rx) = oneshot::channel(); + lock_pending(&pending).insert(req_id, tx); + frame["req_id"] = json!(req_id); + if out.send(frame).is_err() { + lock_pending(&pending).remove(&req_id); + return Err(PeerError::Offline); + } + let response = match tokio::time::timeout(REQUEST_TIMEOUT, rx).await { + Ok(Ok(response)) => response, + Ok(Err(_)) => return Err(PeerError::Rejected("peer link closed".to_string())), + Err(_) => { + lock_pending(&pending).remove(&req_id); + return Err(PeerError::Rejected("peer did not answer".to_string())); + } + }; + if response.get("ok").and_then(Value::as_bool) == Some(true) { + Ok(response.get("result").cloned().unwrap_or(Value::Null)) + } else { + let reason = response + .get("error") + .and_then(Value::as_str) + .unwrap_or("peer refused the request"); + Err(PeerError::Rejected(reason.to_string())) + } + } + + /// Runs an authenticated link until either side closes it. Frames from + /// the peer arrive on `inbound`; frames for it go out on `out`. + pub(super) async fn serve( + &self, + app: Arc, + peer_id: String, + mut inbound: mpsc::UnboundedReceiver, + out: mpsc::UnboundedSender, + ) { + let generation = self.counter.fetch_add(1, Ordering::Relaxed); + let pending: Pending = Arc::default(); + let closed = Arc::new(Notify::new()); + let replaced = self.lock().insert( + peer_id.clone(), + Link { + out: out.clone(), + pending: pending.clone(), + generation, + closed: closed.clone(), + }, + ); + if let Some(old) = replaced { + old.closed.notify_one(); + } + tracing::info!(peer_id, "peer link up"); + let _ = app.db.touch_peer(&peer_id); + + loop { + let frame = tokio::select! { + frame = inbound.recv() => frame, + _ = closed.notified() => None, + }; + let Some(frame) = frame else { + break; + }; + if frame.get("type").and_then(Value::as_str) == Some("response") { + let id = frame.get("req_id").and_then(Value::as_u64); + if let Some(tx) = id.and_then(|id| lock_pending(&pending).remove(&id)) { + let _ = tx.send(frame); + } + continue; + } + // Handled off the read loop, so one slow request (a result with + // artifacts to write) does not hold up the responses behind it. + let app = app.clone(); + let peer_id = peer_id.clone(); + let out = out.clone(); + tokio::task::spawn_blocking(move || { + let req_id = frame.get("req_id").cloned().unwrap_or(Value::Null); + let response = match super::inbound::handle(&app, &peer_id, &frame) { + Ok(result) => json!({ + "type": "response", "req_id": req_id, "ok": true, "result": result + }), + Err(e) => json!({ + "type": "response", "req_id": req_id, "ok": false, + "error": format!("{e:#}") + }), + }; + let _ = out.send(response); + }); + } + + let mut links = self.lock(); + if links + .get(&peer_id) + .is_some_and(|link| link.generation == generation) + { + links.remove(&peer_id); + } + drop(links); + let _ = app.db.touch_peer(&peer_id); + tracing::info!(peer_id, "peer link down"); + } +} + +fn lock_pending( + pending: &Pending, +) -> std::sync::MutexGuard<'_, HashMap>> { + pending.lock().unwrap_or_else(|e| e.into_inner()) +} diff --git a/crates/gravityd/src/peer/inbound.rs b/crates/gravityd/src/peer/inbound.rs new file mode 100644 index 0000000..6901847 --- /dev/null +++ b/crates/gravityd/src/peer/inbound.rs @@ -0,0 +1,76 @@ +//! Requests a peer makes of this daemon. A peer can see which bots exist, +//! link one, and deliver messages to the bots linked to it. Nothing else. + +use std::sync::Arc; + +use bus::{Peer, RemoteBot}; +use serde_json::{json, Value}; + +use crate::app::AppState; + +pub(super) fn handle(app: &Arc, peer_id: &str, frame: &Value) -> anyhow::Result { + let peer = app + .db + .get_peer(peer_id)? + .filter(|p| p.revoked_at.is_none()) + .ok_or_else(|| anyhow::anyhow!("peer revoked"))?; + match frame.get("type").and_then(Value::as_str).unwrap_or("") { + "ping" => Ok(json!({})), + "list_bots" => list_bots(app), + "link" => link(app, &peer, frame), + "message" => { + let message = serde_json::from_value(frame["message"].clone())?; + let received = super::receive::receive(app, &peer, message)?; + Ok(serde_json::to_value(received)?) + } + other => anyhow::bail!("unknown peer request '{other}'"), + } +} + +/// The bots a peer could link: every live bot that runs here. +fn list_bots(app: &Arc) -> anyhow::Result { + let projects: std::collections::HashMap = app + .db + .list_projects()? + .into_iter() + .map(|p| (p.id.clone(), crate::db::Db::display_project_name(&p))) + .collect(); + let bots: Vec = app + .db + .list_bots(None)? + .into_iter() + .filter(|bot| !bot.is_linked()) + .map(|bot| { + let project = projects.get(&bot.project_id).cloned().unwrap_or_default(); + remote_view(&bot, project) + }) + .collect(); + Ok(json!({ "bots": bots })) +} + +/// Exposes one local bot to the peer, which is about to stand it in. +fn link(app: &Arc, peer: &Peer, frame: &Value) -> anyhow::Result { + let bot_id = frame + .get("bot_id") + .and_then(Value::as_str) + .ok_or_else(|| anyhow::anyhow!("'bot_id' is required"))?; + let bot = app + .db + .get_live_bot(bot_id)? + .filter(|b| !b.is_linked()) + .ok_or_else(|| anyhow::anyhow!("no bot with id {bot_id} runs on this daemon"))?; + app.db.expose_bot_to_peer(&peer.id, &bot.id)?; + tracing::info!(peer = %peer.name, bot = %bot.name, "bot linked to peer"); + Ok(json!({ "bot": remote_view(&bot, String::new()) })) +} + +pub(super) fn remote_view(bot: &bus::Bot, project: String) -> RemoteBot { + RemoteBot { + id: bot.id.clone(), + name: bot.name.clone(), + description: bot.description.clone(), + avatar: bot.avatar.clone(), + runtime: bot.runtime, + project, + } +} diff --git a/crates/gravityd/src/peer/receive.rs b/crates/gravityd/src/peer/receive.rs new file mode 100644 index 0000000..80c4dac --- /dev/null +++ b/crates/gravityd/src/peer/receive.rs @@ -0,0 +1,202 @@ +//! Storing a message a peer forwarded: the receiving half of the delivery +//! worker's forward. Each step mirrors what the local tool would have done had +//! the sender been here, so the recipient cannot tell the difference beyond +//! the machine named in the envelope. + +use std::sync::Arc; + +use bus::{MessageKind, Peer, RemoteBot, Sender, SenderKind, TaskState}; + +use crate::app::AppState; +use crate::events::Push; +use crate::messaging::{self, Dm}; + +use super::frames::{FromFrame, MessageFrame, Received}; + +pub(super) fn receive( + app: &Arc, + peer: &Peer, + frame: MessageFrame, +) -> anyhow::Result { + // At-least-once on the wire: an ack lost to a dropped link means the same + // frame again. The first copy's result is the answer. + if let Some(local) = app.db.local_message_for(&peer.id, &frame.id)? { + let task_id = app + .db + .tasks_with_origin(&local)? + .into_iter() + .next() + .map(|t| t.id); + return Ok(Received { + message_id: local, + task_id, + }); + } + + let to = app + .db + .get_live_bot(&frame.to_bot_id)? + .filter(|b| !b.is_linked()) + .ok_or_else(|| anyhow::anyhow!("no bot with id {} runs here", frame.to_bot_id))?; + anyhow::ensure!( + app.db.is_exposed_to_peer(&peer.id, &to.id)?, + "{} is not linked to this daemon", + to.name + ); + + let (sender, linked) = match &frame.from { + FromFrame::User => (messaging::user_sender(), None), + FromFrame::Bot { bot } => { + anyhow::ensure!( + frame.kind != MessageKind::Chat, + "only the owner sends chat messages" + ); + let linked = linked_sender(app, peer, bot, &to.project_id)?; + let sender = Sender { + kind: SenderKind::Bot, + bot_id: Some(linked.id.clone()), + name: linked.name.clone(), + }; + (sender, Some(linked)) + } + }; + let ref_id = match &frame.ref_message_id { + Some(r) => match app.db.local_message_for(&peer.id, r)? { + Some(local) => Some(local), + None => app.db.get_message(r)?.map(|m| m.id), + }, + None => None, + }; + let body = if frame.artifacts.is_empty() { + frame.body.clone() + } else { + super::artifacts::store(app, peer, &to, &frame)? + }; + + let msg = match &frame.closes_task { + Some(closes) => { + let linked = linked + .as_ref() + .ok_or_else(|| anyhow::anyhow!("only a bot closes a task"))?; + let task = app + .db + .get_task(&closes.id)? + .ok_or_else(|| anyhow::anyhow!("no task {} here", closes.id))?; + match closes.state { + TaskState::Done => { + anyhow::ensure!( + task.to_bot_id == linked.id + && task.from_bot_id.as_deref() == Some(to.id.as_str()), + "task {} is not one {} was given by {}", + task.id, + linked.name, + to.name + ); + // Already closed here (expired while the result was in + // flight) still stores the result: the work was done. + app.db.try_close_task(&task.id, TaskState::Done)?; + let origin = app + .db + .get_message(&task.origin_message_id)? + .ok_or_else(|| anyhow::anyhow!("origin message missing"))?; + let msg = app.db.insert_message( + &origin.conversation_id, + &sender, + MessageKind::Done, + &body, + Some(&origin.id), + None, + )?; + app.events.push(Push::MessageNew { + message: msg.clone(), + }); + let key = format!("{}:{}", msg.id, to.id); + let delivery = app.db.enqueue_delivery(&msg.id, &to.id, &key)?; + app.events.push(Push::DeliveryUpdate { delivery }); + msg + } + TaskState::Cancelled => { + anyhow::ensure!( + task.from_bot_id.as_deref() == Some(linked.id.as_str()) + && task.to_bot_id == to.id, + "task {} is not one {} gave {}", + task.id, + linked.name, + to.name + ); + app.db.try_close_task(&task.id, TaskState::Cancelled)?; + let mut dm = Dm::new(&to.id, &sender, frame.kind, &body); + dm.ref_message_id = ref_id.as_deref(); + messaging::send_dm(&app.db, &app.events, dm)? + } + other => anyhow::bail!("a task cannot be closed as {}", other.as_str()), + } + } + None => { + anyhow::ensure!( + frame.kind != MessageKind::Done, + "a result must name the task it closes" + ); + let mut dm = Dm::new(&to.id, &sender, frame.kind, &body); + dm.ref_message_id = ref_id.as_deref(); + messaging::send_dm(&app.db, &app.events, dm)? + } + }; + + // A delegation opens the same task here, so the recipient's + // `complete_task` and every budget work as if the sender were local. The + // hop count carries over, so the chain limit holds across machines. + let mut task_id = None; + if frame.kind == MessageKind::Task { + let (Some(spec), Some(linked)) = (&frame.task, &linked) else { + anyhow::bail!("a task needs a task frame and a bot sender"); + }; + let task = app.db.create_task( + &msg.id, + Some(&linked.id), + &to.id, + spec.deadline_at, + spec.hop_count, + &linked.id, + )?; + app.db.map_peer_task(&peer.id, &spec.id, &task.id)?; + task_id = Some(task.id); + } + app.db.map_peer_message(&peer.id, &frame.id, &msg.id)?; + Ok(Received { + message_id: msg.id, + task_id, + }) +} + +/// The linked bot standing in for the peer's sender, created on first +/// contact. It takes the sender's name, or `-` if a bot here +/// already has it. +fn linked_sender( + app: &Arc, + peer: &Peer, + remote: &RemoteBot, + project_id: &str, +) -> anyhow::Result { + if let Some(bot) = app.db.linked_bot(&peer.id, &remote.id, project_id)? { + return Ok(bot); + } + let mut name = bus::names::validate(&remote.name).map_err(anyhow::Error::msg)?; + if app.db.get_bot_by_name(project_id, &name)?.is_some() { + name = + bus::names::validate(&format!("{name}-{}", peer.name)).map_err(anyhow::Error::msg)?; + anyhow::ensure!( + app.db.get_bot_by_name(project_id, &name)?.is_none(), + "both '{}' and '{name}' are taken in this project", + remote.name + ); + } + let stand_in = RemoteBot { + name, + ..remote.clone() + }; + let bot = app.db.create_linked_bot(project_id, &stand_in, &peer.id)?; + tracing::info!(peer = %peer.name, bot = %bot.name, "linked bot created on first contact"); + app.events.push(Push::BotUpdated { bot: bot.clone() }); + Ok(bot) +} diff --git a/crates/gravityd/src/peer/socket.rs b/crates/gravityd/src/peer/socket.rs new file mode 100644 index 0000000..3d0dd4d --- /dev/null +++ b/crates/gravityd/src/peer/socket.rs @@ -0,0 +1,237 @@ +//! The two ends of a peer link: `/peer` accepts a dialing daemon, and a +//! dialer keeps a connection open to each peer this daemon was told to reach. +//! Both authenticate with a `peer_hello`, then hand the socket to the hub as a +//! pair of frame channels. + +use std::sync::Arc; +use std::time::Duration; + +use axum::extract::ws::{Message as WsMessage, WebSocket, WebSocketUpgrade}; +use axum::extract::State; +use axum::response::IntoResponse; +use futures::{SinkExt, StreamExt}; +use serde_json::{json, Value}; +use tokio::sync::mpsc; +use tokio_tungstenite::tungstenite::protocol::WebSocketConfig; +use tokio_tungstenite::tungstenite::Message as TungMessage; + +use crate::app::AppState; + +/// Version of the frames exchanged on a peer link. +const PEER_PROTOCOL: u64 = 1; + +/// Largest frame on a peer link: a result whose artifacts are at the size +/// cap, base64-encoded, with room for the envelope. +const MAX_PEER_FRAME_BYTES: usize = 80 * 1024 * 1024; + +const HELLO_TIMEOUT: Duration = Duration::from_secs(10); +const MIN_REDIAL: Duration = Duration::from_secs(2); +const MAX_REDIAL: Duration = Duration::from_secs(60); + +pub async fn peer_handler( + State(app): State>, + upgrade: WebSocketUpgrade, +) -> axum::response::Response { + upgrade + .max_message_size(MAX_PEER_FRAME_BYTES) + .max_frame_size(MAX_PEER_FRAME_BYTES) + .on_upgrade(move |socket| accept(app, socket)) + .into_response() +} + +async fn accept(app: Arc, socket: WebSocket) { + let (mut sink, mut stream) = socket.split(); + let hello = match tokio::time::timeout(HELLO_TIMEOUT, stream.next()).await { + Ok(Some(Ok(WsMessage::Text(text)))) => serde_json::from_str::(&text).ok(), + _ => None, + }; + let peer_id = match hello.as_ref().map(|hello| authenticate(&app, hello)) { + Some(Ok(peer_id)) => peer_id, + Some(Err(e)) => { + tracing::warn!(error = %e, "peer handshake refused"); + let refusal = json!({ "type": "error", "message": e.to_string() }); + let _ = sink.send(WsMessage::Text(refusal.to_string())).await; + return; + } + None => return, + }; + let Ok(daemon_id) = app.db.daemon_id() else { + return; + }; + let ok = json!({ "type": "peer_hello_ok", "protocol": PEER_PROTOCOL, "daemon_id": daemon_id }); + if sink.send(WsMessage::Text(ok.to_string())).await.is_err() { + return; + } + + let (in_tx, in_rx) = mpsc::unbounded_channel(); + let (out_tx, mut out_rx) = mpsc::unbounded_channel::(); + let writer = tokio::spawn(async move { + while let Some(frame) = out_rx.recv().await { + if sink.send(WsMessage::Text(frame.to_string())).await.is_err() { + break; + } + } + }); + let reader = tokio::spawn(async move { + while let Some(Ok(frame)) = stream.next().await { + match frame { + WsMessage::Text(text) => { + if let Ok(value) = serde_json::from_str::(&text) { + if in_tx.send(value).is_err() { + break; + } + } + } + WsMessage::Close(_) => break, + _ => {} + } + } + }); + app.peers.serve(app.clone(), peer_id, in_rx, out_tx).await; + reader.abort(); + writer.abort(); +} + +/// The peer a `peer_hello` authenticates as. +fn authenticate(app: &AppState, hello: &Value) -> anyhow::Result { + anyhow::ensure!( + hello.get("type").and_then(Value::as_str) == Some("peer_hello"), + "expected peer_hello" + ); + let protocol = hello.get("protocol").and_then(Value::as_u64).unwrap_or(0); + anyhow::ensure!( + protocol == PEER_PROTOCOL, + "this daemon speaks peer protocol {PEER_PROTOCOL}, not {protocol}" + ); + let token = hello.get("token").and_then(Value::as_str).unwrap_or(""); + let daemon_id = hello + .get("daemon_id") + .and_then(Value::as_str) + .filter(|id| !id.is_empty()) + .ok_or_else(|| anyhow::anyhow!("peer_hello carries no daemon_id"))?; + let peer_id = app + .secrets + .peer_for_token(token) + .ok_or_else(|| anyhow::anyhow!("invalid peer token"))?; + let peer = app + .db + .get_peer(&peer_id)? + .filter(|p| p.revoked_at.is_none()) + .ok_or_else(|| anyhow::anyhow!("peer revoked"))?; + anyhow::ensure!( + app.db.bind_peer_daemon(&peer.id, daemon_id)?, + "this peer token belongs to another daemon" + ); + Ok(peer.id) +} + +/// Starts a dialer for every peer this daemon reaches out to. +pub fn spawn_dialers(app: &Arc) { + match app.db.list_peers() { + Ok(peers) => { + for peer in peers { + if peer.url.is_some() && peer.revoked_at.is_none() { + spawn_dialer(app.clone(), peer.id); + } + } + } + Err(e) => tracing::warn!(error = %e, "could not list peers to dial"), + } +} + +/// Keeps a link open to one peer until it is revoked. +pub fn spawn_dialer(app: Arc, peer_id: String) { + tokio::spawn(async move { + let mut wait = MIN_REDIAL; + loop { + let peer = match app.db.get_peer(&peer_id) { + Ok(Some(peer)) if peer.revoked_at.is_none() => peer, + _ => return, + }; + let (Some(url), Some(token)) = (peer.url.clone(), app.secrets.peer_token(&peer_id)) + else { + return; + }; + match dial(&app, &peer_id, &url, &token).await { + Ok(()) => wait = MIN_REDIAL, + Err(e) => { + tracing::debug!(peer = %peer.name, error = %format!("{e:#}"), "peer dial failed"); + } + } + tokio::time::sleep(wait).await; + wait = (wait * 2).min(MAX_REDIAL); + } + }); +} + +/// One connection attempt, served until it drops. `Ok` once the link was up. +async fn dial(app: &Arc, peer_id: &str, url: &str, token: &str) -> anyhow::Result<()> { + let config = WebSocketConfig { + max_message_size: Some(MAX_PEER_FRAME_BYTES), + max_frame_size: Some(MAX_PEER_FRAME_BYTES), + ..WebSocketConfig::default() + }; + let (socket, _) = tokio_tungstenite::connect_async_with_config(url, Some(config), true).await?; + let (mut sink, mut stream) = socket.split(); + let hello = json!({ + "type": "peer_hello", + "protocol": PEER_PROTOCOL, + "token": token, + "daemon_id": app.db.daemon_id()?, + }); + sink.send(TungMessage::Text(hello.to_string())).await?; + let reply = match tokio::time::timeout(HELLO_TIMEOUT, stream.next()).await { + Ok(Some(Ok(TungMessage::Text(text)))) => serde_json::from_str::(&text)?, + _ => anyhow::bail!("peer closed the link during the handshake"), + }; + if reply.get("type").and_then(Value::as_str) != Some("peer_hello_ok") { + let reason = reply + .get("message") + .and_then(Value::as_str) + .unwrap_or("handshake refused"); + anyhow::bail!("{reason}"); + } + let remote = reply + .get("daemon_id") + .and_then(Value::as_str) + .ok_or_else(|| anyhow::anyhow!("peer did not say which daemon it is"))?; + anyhow::ensure!( + app.db.bind_peer_daemon(peer_id, remote)?, + "the daemon at {url} is not the one this peer was paired with" + ); + + let (in_tx, in_rx) = mpsc::unbounded_channel(); + let (out_tx, mut out_rx) = mpsc::unbounded_channel::(); + let writer = tokio::spawn(async move { + while let Some(frame) = out_rx.recv().await { + if sink + .send(TungMessage::Text(frame.to_string())) + .await + .is_err() + { + break; + } + } + }); + let reader = tokio::spawn(async move { + while let Some(Ok(frame)) = stream.next().await { + match frame { + TungMessage::Text(text) => { + if let Ok(value) = serde_json::from_str::(&text) { + if in_tx.send(value).is_err() { + break; + } + } + } + TungMessage::Close(_) => break, + _ => {} + } + } + }); + app.peers + .serve(app.clone(), peer_id.to_string(), in_rx, out_tx) + .await; + reader.abort(); + writer.abort(); + Ok(()) +} diff --git a/crates/gravityd/src/secrets.rs b/crates/gravityd/src/secrets.rs index 48fbb58..7f3ec72 100644 --- a/crates/gravityd/src/secrets.rs +++ b/crates/gravityd/src/secrets.rs @@ -16,6 +16,9 @@ pub struct Secrets { bot_tokens: Mutex>, /// token -> device_id for device-scoped client credentials. device_tokens: Mutex>, + /// token -> peer_id for the `/peer` link. The same token serves both + /// ends: the listening daemon accepts it, the dialing daemon presents it. + peer_tokens: Mutex>, client_token: String, } @@ -47,6 +50,7 @@ impl Secrets { let mut bot_tokens = HashMap::new(); let mut device_tokens = HashMap::new(); + let mut peer_tokens = HashMap::new(); for entry in fs::read_dir(dir)? { let entry = entry?; let name = entry.file_name().to_string_lossy().to_string(); @@ -66,6 +70,14 @@ impl Secrets { if !token.is_empty() { device_tokens.insert(token, device_id.to_string()); } + } else if let Some(peer_id) = name + .strip_prefix("peer-") + .and_then(|s| s.strip_suffix(".token")) + { + let token = fs::read_to_string(entry.path())?.trim().to_string(); + if !token.is_empty() { + peer_tokens.insert(token, peer_id.to_string()); + } } } @@ -73,6 +85,7 @@ impl Secrets { dir: dir.to_path_buf(), bot_tokens: Mutex::new(bot_tokens), device_tokens: Mutex::new(device_tokens), + peer_tokens: Mutex::new(peer_tokens), client_token, }) } @@ -158,6 +171,49 @@ impl Secrets { } } +impl Secrets { + /// Issue a fresh link token for a peer that will dial this daemon. + pub fn issue_peer_token(&self, peer_id: &str) -> anyhow::Result { + let token = random_token(); + self.store_peer_token(peer_id, &token)?; + Ok(token) + } + + /// Keep the token from a peer's invite, presented when dialing it. + pub fn store_peer_token(&self, peer_id: &str, token: &str) -> anyhow::Result<()> { + let mut map = self.peer_tokens.lock().unwrap_or_else(|e| e.into_inner()); + map.retain(|_, id| id.as_str() != peer_id); + write_secret(&self.dir.join(format!("peer-{peer_id}.token")), token)?; + map.insert(token.to_string(), peer_id.to_string()); + Ok(()) + } + + pub fn peer_for_token(&self, token: &str) -> Option { + let map = self.peer_tokens.lock().unwrap_or_else(|e| e.into_inner()); + map.iter() + .find(|(t, _)| constant_time_eq(t.as_bytes(), token.as_bytes())) + .map(|(_, id)| id.clone()) + } + + pub fn peer_token(&self, peer_id: &str) -> Option { + let map = self.peer_tokens.lock().unwrap_or_else(|e| e.into_inner()); + map.iter() + .find(|(_, id)| id.as_str() == peer_id) + .map(|(t, _)| t.clone()) + } + + /// Delete a revoked peer's token so neither end can use the link again. + pub fn remove_peer_token(&self, peer_id: &str) -> anyhow::Result<()> { + let mut map = self.peer_tokens.lock().unwrap_or_else(|e| e.into_inner()); + map.retain(|_, id| id.as_str() != peer_id); + let path = self.dir.join(format!("peer-{peer_id}.token")); + if path.exists() { + fs::remove_file(path)?; + } + Ok(()) + } +} + fn constant_time_eq(a: &[u8], b: &[u8]) -> bool { if a.len() != b.len() { return false; diff --git a/crates/gravityd/src/server.rs b/crates/gravityd/src/server.rs index e2845ca..a5d5ebe 100644 --- a/crates/gravityd/src/server.rs +++ b/crates/gravityd/src/server.rs @@ -25,6 +25,7 @@ pub fn router(app: Arc) -> Router { .route("/ws", get(crate::ws::ws_handler)) .route("/mcp", post(crate::mcp::mcp_handler)) .route("/hook", post(crate::mcp::hook_handler)) + .route("/peer", get(crate::peer::peer_handler)) .with_state(app) } @@ -72,7 +73,10 @@ pub fn spawn_workers(app: &Arc) { }); } + crate::peer::spawn_dialers(app); + let worker = DeliveryWorker { + app: app.clone(), db: app.db.clone(), supervisor: app.supervisor.clone(), events: app.events.clone(), diff --git a/crates/gravityd/src/supervisor/lifecycle.rs b/crates/gravityd/src/supervisor/lifecycle.rs index 98328cf..007cf59 100644 --- a/crates/gravityd/src/supervisor/lifecycle.rs +++ b/crates/gravityd/src/supervisor/lifecycle.rs @@ -9,6 +9,9 @@ use super::*; impl Supervisor { pub fn start_bot(&self, bot_id: &str) -> anyhow::Result<()> { let bot = self.inner.db.get_bot(bot_id)?.context("bot not found")?; + if bot.is_linked() { + return Ok(()); // runs on its peer; nothing to start here + } // Creation and the supervision tick can both start a new bot. Held // until this call returns, so a second start finds the claim and @@ -228,7 +231,7 @@ impl Supervisor { return; } }; - for bot in bots { + for bot in bots.into_iter().filter(|b| !b.is_linked()) { if !self.wants_start(&bot.id) { continue; } diff --git a/crates/gravityd/src/ws.rs b/crates/gravityd/src/ws.rs index 72c0b7d..7187eb9 100644 --- a/crates/gravityd/src/ws.rs +++ b/crates/gravityd/src/ws.rs @@ -23,6 +23,7 @@ mod decisions_publish; mod dispatch; mod entities; mod messaging; +mod peers; mod routines; mod runtime; mod terminal; diff --git a/crates/gravityd/src/ws/dispatch.rs b/crates/gravityd/src/ws/dispatch.rs index 658f2e0..0a70511 100644 --- a/crates/gravityd/src/ws/dispatch.rs +++ b/crates/gravityd/src/ws/dispatch.rs @@ -30,6 +30,8 @@ const READ_ONLY: &[&str] = &[ "get_decision", "list_tags", "count_pending_decisions", + "list_peers", + "list_peer_bots", ]; /// Requests that exercise the owner's ruling authority. @@ -130,6 +132,12 @@ impl Conn { "rename_tag" => self.rename_tag(&req_id, req), "delete_tag" => self.delete_tag(&req_id, req), "set_project_lead" => self.set_project_lead(&req_id, req), + "list_peers" => self.list_peers(&req_id), + "create_peer_invite" => self.create_peer_invite(&req_id, req), + "add_peer" => self.add_peer(&req_id, req), + "revoke_peer" => self.revoke_peer(&req_id, req), + "list_peer_bots" => self.list_peer_bots(&req_id, req), + "link_peer_bot" => self.link_peer_bot(&req_id, req), other => { self.reply_err( &req_id, diff --git a/crates/gravityd/src/ws/peers.rs b/crates/gravityd/src/ws/peers.rs new file mode 100644 index 0000000..350b152 --- /dev/null +++ b/crates/gravityd/src/ws/peers.rs @@ -0,0 +1,197 @@ +//! Peer requests: pairing with another daemon, and linking its bots into a +//! project here. See `docs/peer-bots.md`. + +use std::net::SocketAddr; +use std::sync::Arc; + +use bus::RemoteBot; +use serde_json::{json, Value}; + +use crate::app::AppState; +use crate::events::Push; + +use super::Conn; + +impl Conn { + pub(super) fn list_peers(&self, req_id: &Value) -> anyhow::Result<()> { + let peers: Vec = self + .app + .db + .list_peers()? + .into_iter() + .map(|peer| peer_view(&self.app, &peer)) + .collect(); + self.send(json!({ "type": "peers", "req_id": req_id, "peers": peers })); + Ok(()) + } + + /// Pairing, listening side: registers the machine that will dial in and + /// returns the code to paste into it. The code holds the token, so it is + /// shown once, like a device token. + pub(super) fn create_peer_invite(&self, req_id: &Value, req: &Value) -> anyhow::Result<()> { + let name = peer_name(&self.app, Self::str_field(req, "name")?)?; + let url = match req.get("url").and_then(Value::as_str) { + Some(url) => url.to_string(), + None => listening_url(&self.app)?, + }; + let peer = self.app.db.create_peer(&name, None)?; + let token = self.app.secrets.issue_peer_token(&peer.id)?; + self.send(json!({ + "type": "peer", "req_id": req_id, + "peer": peer_view(&self.app, &peer), + "invite": format!("{url}#{token}") + })); + Ok(()) + } + + /// Pairing, dialing side: stores the other daemon's invite and starts + /// keeping a link open to it. + pub(super) fn add_peer(&self, req_id: &Value, req: &Value) -> anyhow::Result<()> { + let name = peer_name(&self.app, Self::str_field(req, "name")?)?; + let invite = Self::str_field(req, "invite")?.trim(); + let (url, token) = invite + .rsplit_once('#') + .filter(|(url, token)| { + (url.starts_with("ws://") || url.starts_with("wss://")) && !token.is_empty() + }) + .ok_or_else(|| { + anyhow::anyhow!("not a peer invite: expected ws://host:port/peer#token") + })?; + let peer = self.app.db.create_peer(&name, Some(url))?; + self.app.secrets.store_peer_token(&peer.id, token)?; + crate::peer::spawn_dialer(self.app.clone(), peer.id.clone()); + self.send(json!({ "type": "peer", "req_id": req_id, "peer": peer_view(&self.app, &peer) })); + Ok(()) + } + + pub(super) fn revoke_peer(&self, req_id: &Value, req: &Value) -> anyhow::Result<()> { + let peer_id = Self::str_field(req, "peer_id")?; + if !self.app.db.revoke_peer(peer_id)? { + self.reply_err(req_id, "not_found", "peer not found or already revoked"); + return Ok(()); + } + self.app.secrets.remove_peer_token(peer_id)?; + self.app.peers.disconnect(peer_id); + let peer = self + .app + .db + .get_peer(peer_id)? + .ok_or_else(|| anyhow::anyhow!("peer vanished"))?; + self.send(json!({ "type": "peer", "req_id": req_id, "peer": peer_view(&self.app, &peer) })); + Ok(()) + } + + /// The bots running on a peer, for choosing one to link. + pub(super) fn list_peer_bots(&self, req_id: &Value, req: &Value) -> anyhow::Result<()> { + let peer_id = Self::str_field(req, "peer_id")?.to_string(); + let (app, out, req_id) = (self.app.clone(), self.out.clone(), req_id.clone()); + tokio::spawn(async move { + let reply = match app + .peers + .request(&peer_id, json!({ "type": "list_bots" })) + .await + { + Ok(result) => json!({ + "type": "peer_bots", "req_id": req_id, "peer_id": peer_id, + "bots": result.get("bots").cloned().unwrap_or_else(|| json!([])) + }), + Err(e) => error(&req_id, "unavailable", &e.to_string()), + }; + let _ = out.send(reply); + }); + Ok(()) + } + + /// Stands a peer's bot in as a linked bot in a project here. + pub(super) fn link_peer_bot(&self, req_id: &Value, req: &Value) -> anyhow::Result<()> { + let peer_id = Self::str_field(req, "peer_id")?.to_string(); + let remote_bot_id = Self::str_field(req, "remote_bot_id")?.to_string(); + let project_id = Self::str_field(req, "project_id")?.to_string(); + let (app, out, req_id) = (self.app.clone(), self.out.clone(), req_id.clone()); + tokio::spawn(async move { + let reply = match link(&app, &peer_id, &remote_bot_id, &project_id).await { + Ok(bot) => json!({ + "type": "bot", "req_id": req_id, "bot": super::views::bot_view(&app, &bot) + }), + Err(e) => error(&req_id, "conflict", &format!("{e:#}")), + }; + let _ = out.send(reply); + }); + Ok(()) + } +} + +async fn link( + app: &Arc, + peer_id: &str, + remote_bot_id: &str, + project_id: &str, +) -> anyhow::Result { + let project = app + .db + .get_project(project_id)? + .filter(|p| p.deleted_at.is_none()) + .ok_or_else(|| anyhow::anyhow!("project not found"))?; + if let Some(bot) = app.db.linked_bot(peer_id, remote_bot_id, &project.id)? { + return Ok(bot); + } + let result = app + .peers + .request(peer_id, json!({ "type": "link", "bot_id": remote_bot_id })) + .await + .map_err(|e| anyhow::anyhow!("{e}"))?; + let remote: RemoteBot = serde_json::from_value(result["bot"].clone())?; + // Bots address each other by name, so a clash would make one of the two + // unreachable. The owner renames one and links again. + anyhow::ensure!( + app.db.get_bot_by_name(&project.id, &remote.name)?.is_none(), + "a bot named '{}' already exists in this project; rename it first", + remote.name + ); + let bot = app.db.create_linked_bot(&project.id, &remote, peer_id)?; + app.events.push(Push::BotUpdated { bot: bot.clone() }); + Ok(bot) +} + +fn peer_name(app: &AppState, raw: &str) -> anyhow::Result { + let name = bus::names::validate_project(raw).map_err(anyhow::Error::msg)?; + anyhow::ensure!( + app.db.get_peer_by_name(&name)?.is_none(), + "a peer named '{name}' already exists" + ); + Ok(name) +} + +/// The URL another daemon dials to reach this one: its first non-loopback +/// bind address, which is the Tailscale interface in the documented setup. +fn listening_url(app: &AppState) -> anyhow::Result { + let ip = app + .cfg + .bind + .iter() + .find(|ip| !ip.is_loopback()) + .ok_or_else(|| { + anyhow::anyhow!( + "this daemon only listens on localhost; add its Tailscale address to \ + `bind` in gravityd.toml and restart it, or pass the url to dial" + ) + })?; + Ok(format!("ws://{}/peer", SocketAddr::new(*ip, app.cfg.port))) +} + +pub(crate) fn peer_view(app: &AppState, peer: &bus::Peer) -> Value { + json!({ + "id": peer.id, + "name": peer.name, + "url": peer.url, + "daemon_id": peer.daemon_id, + "online": app.peers.is_online(&peer.id), + "created_at": peer.created_at.to_rfc3339(), + "last_seen_at": peer.last_seen_at.map(|t| t.to_rfc3339()), + "revoked_at": peer.revoked_at.map(|t| t.to_rfc3339()) + }) +} + +fn error(req_id: &Value, code: &str, message: &str) -> Value { + json!({ "type": "error", "req_id": req_id, "code": code, "message": message }) +} diff --git a/crates/gravityd/src/ws/views.rs b/crates/gravityd/src/ws/views.rs index 612d5af..3b1881e 100644 --- a/crates/gravityd/src/ws/views.rs +++ b/crates/gravityd/src/ws/views.rs @@ -24,9 +24,31 @@ pub(crate) fn project_view(project: &bus::Project) -> Value { /// A bot as clients see it: the stored row plus the runtime fields the /// supervisor and the delivery tables own. pub(crate) fn bot_view(app: &AppState, bot: &bus::Bot) -> Value { - let (state, reason) = app.supervisor.state(&bot.id); + let (mut state, mut reason) = app.supervisor.state(&bot.id); let unread = app.db.unread_count(&bot.id).unwrap_or(0); + // A linked bot has no session here; what matters is whether its machine + // is reachable. + let peer = bot + .peer_id + .as_deref() + .and_then(|id| app.db.get_peer(id).ok().flatten()); + if let Some(peer) = &peer { + let online = app.peers.is_online(&peer.id); + state = if online { + bus::BotState::Ready + } else { + bus::BotState::Stopped + }; + reason = format!( + "runs on {}{}", + peer.name, + if online { "" } else { " (offline)" } + ); + } json!({ + "peer": peer.as_ref().map(|p| json!({ + "id": p.id, "name": p.name, "online": app.peers.is_online(&p.id) + })), "id": bot.id, "project_id": bot.project_id, // Archived rows carry a tombstoned name so the original is free to diff --git a/crates/gravityd/tests/peer_bots.rs b/crates/gravityd/tests/peer_bots.rs new file mode 100644 index 0000000..283d5f5 --- /dev/null +++ b/crates/gravityd/tests/peer_bots.rs @@ -0,0 +1,322 @@ +//! Peer bots: two daemons paired over a peer link, a lead on one delegating +//! to a bot on the other. Both daemons run in this process on the runtime +//! double, so the whole path — pairing, linking, forwarding, task mirroring, +//! artifact transfer — runs for real without a second machine. + +mod common; + +use std::time::Duration; + +use bus::TaskState; +use common::tasks::{drain_until, error_text}; +use common::*; +use serde_json::{json, Value}; + +struct Team { + mac: TestDaemon, + win: TestDaemon, + mac_client: WsClient, + /// The Windows daemon's id for the peer row standing for the Mac. + win_peer_id: String, + /// The Mac's linked bot standing in for `windev`. + linked_windev: String, + lead: McpClient, + windev: McpClient, + windev_id: String, +} + +async fn wait_until(what: &str, mut check: impl FnMut() -> bool) { + let deadline = tokio::time::Instant::now() + Duration::from_secs(5); + while !check() { + assert!(tokio::time::Instant::now() < deadline, "timed out: {what}"); + tokio::time::sleep(Duration::from_millis(25)).await; + } +} + +async fn project(c: &mut WsClient, name: &str) -> String { + let created = c + .request(json!({"type": "create_project", "name": name})) + .await; + created["project"]["id"].as_str().expect("pid").to_string() +} + +/// A lead on "the Mac" and a Windows developer on "the PC", paired, with the +/// developer linked into the lead's project. +async fn team() -> Team { + let mac = spawn_daemon().await; + let win = spawn_daemon().await; + let mut mac_client = WsClient::connect(&mac).await; + let mut win_client = WsClient::connect(&win).await; + + let mac_project = project(&mut mac_client, "app").await; + let lead = create_bot(&mut mac_client, &mac_project, "lead").await; + let win_project = project(&mut win_client, "app").await; + let windev = create_bot(&mut win_client, &win_project, "windev").await; + let windev_id = windev["id"].as_str().expect("id").to_string(); + + let invite = win_client + .request(json!({ + "type": "create_peer_invite", "name": "mac", + "url": format!("ws://{}/peer", win.addr) + })) + .await; + assert_eq!(invite["type"], "peer", "{invite}"); + let win_peer_id = invite["peer"]["id"].as_str().expect("peer id").to_string(); + let added = mac_client + .request(json!({ + "type": "add_peer", "name": "win", "invite": invite["invite"] + })) + .await; + assert_eq!(added["type"], "peer", "{added}"); + let mac_peer_id = added["peer"]["id"].as_str().expect("peer id").to_string(); + wait_until("the link comes up", || { + mac.app.peers.is_online(&mac_peer_id) + }) + .await; + + let bots = mac_client + .request(json!({"type": "list_peer_bots", "peer_id": mac_peer_id})) + .await; + assert_eq!(bots["bots"][0]["name"], "windev", "{bots}"); + let linked = mac_client + .request(json!({ + "type": "link_peer_bot", "peer_id": mac_peer_id, + "remote_bot_id": windev_id, "project_id": mac_project + })) + .await; + assert_eq!(linked["type"], "bot", "{linked}"); + assert_eq!(linked["bot"]["peer"]["name"], "win"); + + let lead_token = mac + .app + .secrets + .bot_token(lead["id"].as_str().expect("id")) + .expect("token"); + let windev_token = win.app.secrets.bot_token(&windev_id).expect("token"); + Team { + lead: McpClient::new(&mac, &lead_token), + windev: McpClient::new(&win, &windev_token), + linked_windev: linked["bot"]["id"].as_str().expect("id").to_string(), + mac, + win, + mac_client, + win_peer_id, + windev_id, + } +} + +fn find<'a>(messages: &'a [Value], needle: &str) -> &'a Value { + messages + .iter() + .find(|m| m["body"].as_str().is_some_and(|b| b.contains(needle))) + .unwrap_or_else(|| panic!("no message containing {needle:?} in {messages:?}")) +} + +#[tokio::test] +async fn a_task_crosses_to_the_peer_and_its_result_and_files_come_back() { + let mut t = team().await; + + let bots = t.lead.call("list_bots", json!({})).await; + let listed = bots["bots"] + .as_array() + .expect("bots") + .iter() + .find(|b| b["name"] == "windev") + .expect("windev listed"); + assert_eq!(listed["machine"], "win"); + assert_eq!(listed["online"], true); + + let sent = t + .lead + .call( + "send_message", + json!({"to": "windev", "kind": "task", "body": "build the windows installer"}), + ) + .await; + let mac_task = sent["task_id"].as_str().expect("task id").to_string(); + + // On the PC the task arrives from a linked "lead", with a task of its own. + let inbox = drain_until(&mut t.windev, "build the windows installer").await; + let task_msg = find(&inbox, "build the windows installer"); + assert_eq!(task_msg["from"], "lead"); + let win_task = task_msg["task_id"] + .as_str() + .expect("mirrored task") + .to_string(); + + let windev = t + .win + .app + .db + .get_bot(&t.windev_id) + .expect("db") + .expect("bot"); + let out = std::path::Path::new(&windev.workspace_path).join("out"); + std::fs::create_dir_all(&out).expect("mkdir"); + std::fs::write(out.join("setup.txt"), "installer bytes").expect("write"); + t.windev + .call( + "complete_task", + json!({ + "task_id": win_task, "result": "installer built", + "artifacts": ["out/setup.txt", "../../../../secrets/client.token"] + }), + ) + .await; + + // Back on the Mac: the lead's task is closed by the peer's result, and + // the file it listed is on this disk. + let inbox = drain_until(&mut t.lead, "installer built").await; + let done = find(&inbox, "installer built"); + assert_eq!(done["kind"], "done"); + let body = done["body"].as_str().expect("body"); + let local = body + .lines() + .find_map(|l| l.strip_prefix("- ").filter(|p| p.ends_with("setup.txt"))) + .unwrap_or_else(|| panic!("no transferred file in {body}")); + assert_eq!( + std::fs::read_to_string(local).expect("artifact"), + "installer bytes" + ); + // A path outside the bot's own files is listed, never sent. + assert!(body.contains("not transferred"), "{body}"); + let task = t.mac.app.db.get_task(&mac_task).expect("db").expect("task"); + assert_eq!(task.state, TaskState::Done); + + // The lead's session saw the sender's machine in the envelope. + let lead_id = t + .mac + .app + .db + .get_task(&mac_task) + .unwrap() + .unwrap() + .from_bot_id + .unwrap(); + wait_until("the result reaches the lead's session", || { + terminal(&t.mac, &lead_id).contains("from WINDEV @ WIN · done") + }) + .await; +} + +#[tokio::test] +async fn cancelling_on_one_side_closes_the_task_on_the_other() { + let mut t = team().await; + let sent = t + .lead + .call( + "send_message", + json!({"to": "windev", "kind": "task", "body": "port the updater"}), + ) + .await; + let inbox = drain_until(&mut t.windev, "port the updater").await; + let win_task = find(&inbox, "port the updater")["task_id"] + .as_str() + .expect("task id") + .to_string(); + + t.lead + .call( + "cancel_task", + json!({"task_id": sent["task_id"], "reason": "not needed"}), + ) + .await; + drain_until(&mut t.windev, "was cancelled").await; + let task = t.win.app.db.get_task(&win_task).expect("db").expect("task"); + assert_eq!(task.state, TaskState::Cancelled); + let refused = t + .windev + .call_raw("complete_task", json!({"task_id": win_task, "result": "x"})) + .await; + assert!(error_text(&refused).contains("already cancelled")); +} + +#[tokio::test] +async fn the_owner_can_message_a_linked_bot_from_the_other_machine() { + let mut t = team().await; + let sent = t + .mac_client + .request(json!({ + "type": "send_user_message", "to_bot_id": t.linked_windev, + "body": "which SDK are you on?" + })) + .await; + assert_eq!(sent["type"], "message", "{sent}"); + let inbox = drain_until(&mut t.windev, "which SDK").await; + let chat = find(&inbox, "which SDK"); + assert_eq!(chat["from"], "user"); + assert_eq!(chat["kind"], "chat"); +} + +#[tokio::test] +async fn a_revoked_peer_is_cut_off() { + let mut t = team().await; + let mut win_client = WsClient::connect(&t.win).await; + let revoked = win_client + .request(json!({"type": "revoke_peer", "peer_id": t.win_peer_id})) + .await; + assert_eq!(revoked["type"], "peer", "{revoked}"); + let mac = &t.mac; + wait_until("the Mac sees the link drop", || { + mac.app + .db + .list_peers() + .expect("peers") + .iter() + .all(|p| !mac.app.peers.is_online(&p.id)) + }) + .await; + + // Messages wait rather than fail while the peer is unreachable. + t.lead + .call( + "send_message", + json!({"to": "windev", "kind": "note", "body": "ping"}), + ) + .await; + tokio::time::sleep(Duration::from_millis(400)).await; + let queued = t + .mac + .app + .db + .list_deliveries(Some(&t.linked_windev), None) + .expect("deliveries"); + assert!( + queued + .iter() + .all(|d| d.state != bus::DeliveryState::Delivered), + "{queued:?}" + ); +} + +#[tokio::test] +async fn the_peer_bot_can_ask_back_on_an_open_task() { + let mut t = team().await; + t.lead + .call( + "send_message", + json!({"to": "windev", "kind": "task", "body": "sign the binaries"}), + ) + .await; + drain_until(&mut t.windev, "sign the binaries").await; + t.windev + .call( + "send_message", + json!({"to": "lead", "kind": "reply", "body": "which certificate?"}), + ) + .await; + let inbox = drain_until(&mut t.lead, "which certificate?").await; + let question = find(&inbox, "which certificate?"); + assert_eq!(question["kind"], "reply"); + assert_eq!(question["from"], "windev"); + + // Handing the work back up the chain is still a loop across machines. + let refused = t + .windev + .call_raw( + "send_message", + json!({"to": "lead", "kind": "task", "body": "you do it"}), + ) + .await; + assert!(error_text(&refused).contains("loop")); +} diff --git a/docs/architecture.md b/docs/architecture.md index bbf4cc9..a0da928 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -106,7 +106,9 @@ an Agent SDK adapter are not implemented runtime choices. Messages travel through the durable bus. A user can send a message through the control plane; a bot uses MCP tools such as `send_message`, `check_inbox`, and -`complete_task`. Bot-to-bot addressing is scoped to a project. +`complete_task`. Bot-to-bot addressing is scoped to a project. A project can +include linked bots that run on a paired peer daemon; see +[peer bots](peer-bots.md). The delivery path is: diff --git a/docs/peer-bots.md b/docs/peer-bots.md index b40de06..d831ff8 100644 --- a/docs/peer-bots.md +++ b/docs/peer-bots.md @@ -1,6 +1,9 @@ # Peer bots: one team across two machines -Status: design, not yet implemented. +Status: the daemon side (pairing, linking, forwarding, task mirroring and +artifact transfer) is implemented and covered by `crates/gravityd/tests/peer_bots.rs`. +The desktop UI is not built yet; pair and link with `gravityd peer …` until +it is. Requests are listed in [protocol.md](protocol.md). A team can span two Gravity daemons. The motivating case is an app built for macOS and Windows: a lead and a Mac developer run on the Mac, a Windows @@ -12,6 +15,30 @@ Each bot still runs, with its own terminal and runtime, on the machine that owns it. What crosses machines is the bus: messages, tasks, results, and the artifacts those results attach. +## Setting it up + +On the Windows PC, add its Tailscale address to `bind` in `gravityd.toml` +(next to `127.0.0.1`) and restart the daemon. Then create an invite for the +Mac: + +```sh +gravityd peer invite mac +``` + +On the Mac, add the PC with that invite, check that the link is up, and link +the Windows bot into the project the lead works in: + +```sh +gravityd peer add win-pc "ws://100.x.y.z:49777/peer#…" +gravityd peer list +gravityd peer bots win-pc +gravityd peer link win-pc windev --project my-app +``` + +The lead can now `send_message(to: "windev", kind: "task", …)`. The first +message creates a linked `lead` in the Windows bot's project, so its answers +come back without any setup on the PC. + ## Assumptions - Both daemons belong to one owner and reach each other over Tailscale. Like diff --git a/docs/protocol.md b/docs/protocol.md index a1f254c..dc377ac 100644 --- a/docs/protocol.md +++ b/docs/protocol.md @@ -83,6 +83,12 @@ Codes: `auth_failed`, `unsupported_version`, `not_found`, `invalid_request`, | `list_devices` | – | `devices` | | `create_device` | `name, capabilities` (array of `"read"`/`"control"`/`"approve"`) | `device` (includes `token`, shown once) | | `revoke_device` | `device_id` | `device` | +| `list_peers` | – | `peers` | +| `create_peer_invite` | `name, url?` (defaults to the first non-loopback bind address) | `peer` plus `invite` (holds the link token, shown once) | +| `add_peer` | `name, invite` | `peer`; the daemon starts dialing it | +| `revoke_peer` | `peer_id` | `peer` | +| `list_peer_bots` | `peer_id` | `peer_bots` (`bots`: id, name, description, avatar, runtime, project) | +| `link_peer_bot` | `peer_id, remote_bot_id, project_id` | `bot` (a linked bot; `peer` is set on it) | | `list_decisions` | `project_id?, state?, tag?, bot_id?, query?, before?, limit?` | `decisions` | | `get_decision` | `decision_id` | `decision` (with comments, tags, notifications) | | `count_pending_decisions` | – | `pending_decisions` | diff --git a/third-party/inventory.json b/third-party/inventory.json index 1d7143a..72dd3d2 100644 --- a/third-party/inventory.json +++ b/third-party/inventory.json @@ -4,9 +4,9 @@ "x86_64-pc-windows-msvc" ], "inputs": { - "Cargo.lock": "ed089d7f431124cb093bc49f5c54a6ca4cb0af573ce8be2f6386475d742fca84", + "Cargo.lock": "2d03468c3b96ec984adfa2e515c1f8a20a6fa7dc39b3b2753b350085647f75c9", "Cargo.toml": "17f46fbe0ce2a8fab2e1b26a1b10435ac1968ae6f8150db33bb1e05c25fb7491", - "crates/gravityd/Cargo.toml": "94c7560d2df9a9f0c377739a0f858b6af099db9ff5de3789f6e1876e0169ad8e", + "crates/gravityd/Cargo.toml": "e948b84372298a104eba4ea6dd4e76cd75e2c16278587dae0a51368943d4c2a7", "crates/bus/Cargo.toml": "c4c4f51131d78f8ae8cb6e96fcb918c245998d9c1e01312f513fb56dfc452910", "apps/desktop/src-tauri/Cargo.lock": "7c73bbce32b88bd02862bcf73cea344288764824065d4379787d9c0de7991474", "apps/desktop/src-tauri/Cargo.toml": "421dc0d9bcf49e5d93eb01a987ef7b77b393e1e383be7eac64cabde2daf53702", From c40260941fdc6cc5b5a2228c6f48edba3e76dea7 Mon Sep 17 00:00:00 2001 From: Manuel Rinaldi Date: Thu, 1 Oct 2026 18:53:24 +0300 Subject: [PATCH 15/55] docs: design the chat pane as the default bot surface Refs ahilles107/gravity#13. Co-Authored-By: Claude Opus 5.5 --- .../specs/2026-09-16-chat-pane-design.md | 356 ++++++++++++++++++ 1 file changed, 356 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-16-chat-pane-design.md diff --git a/docs/superpowers/specs/2026-09-16-chat-pane-design.md b/docs/superpowers/specs/2026-09-16-chat-pane-design.md new file mode 100644 index 0000000..79b601d --- /dev/null +++ b/docs/superpowers/specs/2026-09-16-chat-pane-design.md @@ -0,0 +1,356 @@ +# Chat pane: the default bot surface + +Issue: [ahilles107/gravity#13](https://github.com/ahilles107/gravity/issues/13). +Status: design. Branch: `codex/chat-pane` (stacked on `codex/peer-bots`). + +Opening a bot today shows its terminal. The terminal is the truth, but it is a +poor way to read what a bot did. Images are invisible, diffs scroll past, files +it wrote are paths you have to go and open, and a permission prompt is a +keystroke you have to type. This design makes a chat the default surface. The +chat has message bubbles, folded tool steps, inline diffs and images, an +artifacts panel, and permission cards that actually decide the tool. The +Claude Code terminal stays one tab away. + +The issue's v1 is the core. This document also covers what the issue left out, +because the goal is a full rework of how a bot is read and driven, not a +second view bolted on. + +## What exists today + +- **Desktop.** `BotView` has two tabs, Terminal and Routines, plus an info + panel. Nothing renders a bot's conversation, and nothing calls + `send_user_message`. A permission prompt raises a toast and an amber dot, + and the only way to answer it is to type into the terminal. Bot markdown is + rendered only in the Control Center, with links and images turned into + inert text by policy. +- **Daemon.** + - `activity.rs` finds a bot's Claude Code transcript + (`~/.claude/projects//*.jsonl`, newest file) but only + reads the newest assistant text, for the sidebar preview. + - Hooks are fire-and-forget curls for six events, and no hook ever returns + a decision. + - Codex bots write `codex-observations.jsonl`, which holds text only. + - Nothing serves a file's bytes to a client. +- **gravity_os** (the iPhone app) reads turns, diffs, screenshots and reports. + It gets them from **Gravity Lens**, a companion Python service that parses + the same transcripts because the daemon does not serve them. Its turn model + is the best prior art we have, and this design moves it into the daemon so + the desktop and the phone read one source. + +## Principles + +1. **The transcript is the log.** The human↔bot conversation is read from the + runtime's own record. It is never reconstructed from terminal output, and + it is never duplicated into the bus database. +2. **The daemon parses, clients render.** One parser in Rust, one wire model. + The desktop, gravity_os and any future client read the same thing. +3. **Additive protocol.** New requests, pushes and a `chat` capability. The + protocol stays at version 2. Old clients keep working. New clients hide the + chat when the capability is absent and fall back to the terminal. +4. **The terminal stays authoritative.** Anything the chat cannot do, such as + slash commands, raw output or an unanswered prompt, is one tab away, and + the chat says when to go there. +5. **Permission answers never default to yes.** A card that is not answered in + time is a deny. If the daemon cannot be reached, Claude Code's own prompt + appears in the terminal. Nothing persists an always-allow. + +## The wire model + +A bot's chat is a list of **turns**. A turn starts with what woke the bot and +ends when the runtime reports it stopped. This is the shape gravity_os already +proved on a phone. + +```text +ChatTurn { + id // stable: the transcript uuid of the first record + bot_id + started_at, ended_at? // ended_at absent while the turn is open + open: bool + trigger: Trigger + items: [ChatItem] // in transcript order + stats: { commands, reads, edits, added, removed, sent, images, errors } +} + +Trigger = + | Owner { text, via: "chat" | "terminal" } // composer, or typed in the PTY + | Bus { from, kind, num, text, task_id? } // another bot's message + | Routine { name, run_id? } + | Ruling { decision_id } // a decision answer delivered + | Resumed // a turn continued after compaction + | Background { text } // task notification, queued command + +ChatItem = + | Text { id, markdown } // consecutive assistant text merges + | Step { id, tool, title, subtitle?, status: running|ok|error|denied, + added?, removed?, images: [ImageRef], detail: bool } + | Sent { id, to, kind, body } // send_message on the bus + | Completed { id, task_id, result, artifacts: [FileRef] } + | Decision { id, decision_id, title } // raise_decision + | Permission { id, request_id, tool, summary, state } // see Permissions + | Aside { id, kind: compacted|interrupted|incoming|model, text } +``` + +`Step.title` is human ("Edited `src/app.ts`", "Ran tests"), using the +descriptions bots already give their Bash calls. Gravity's own bus tools are +not steps: they become `Sent`, `Completed` and `Decision` items, because those +are what the owner wants to see. Housekeeping tools (`check_inbox`, +`list_bots`, ToolSearch, Skill) are steps marked minor and fold away. + +Heavy content is not in the turn. It is fetched on demand: + +- **Step detail.** Tool input, command, output (tail-truncated), and the diff. + The diff comes from Claude Code's `toolUseResult.structuredPatch`, not from + re-diffing the Edit inputs, and is capped at 400 lines. +- **Images.** Base64 image blocks inside tool results, plus image paths + mentioned by steps (screenshots a bot took), served by id with a thumbnail + size. +- **Files.** Artifacts and any file under the bot's directory or its project's + artifacts directory. + +### Recognising who spoke + +The parser classifies user records with markers Claude Code already writes: + +- **Typed in the terminal:** `origin.kind == "human"` → `Owner{via: terminal}`. +- **Delivered by Gravity:** `isMeta` with `origin.kind == "peer"`, wrapped in + "Another Claude session sent a message:" plus a trailer. The parser strips + the wrapper and reads the envelope (`[msg #N from X · kind · …]`): + - `from USER · chat` is the owner writing from the composer + (`Owner{via: chat}`); + - any other sender is a `Bus` trigger; + - `[routine "…"]` is a `Routine` trigger. +- **Mid-turn deliveries** (queued commands, a message arriving while the bot + works) become `Aside{incoming}` inside the open turn rather than starting a + new one. +- Sidechain records (sub-agents) are skipped in v1 and summarised by the step + that launched them. + +## Protocol additions + +New capability `chat` in `hello_ok`. All additive. + +| Request | Grant | Fields | Reply | +| --- | --- | --- | --- | +| `list_chat` | read | `bot_id, before?, limit?` | `chat { bot_id, turns, has_more }`, newest last | +| `get_chat_step` | read | `bot_id, turn_id, item_id` | `chat_step { input?, command?, output?, diff?, content? }` | +| `get_chat_image` | read | `bot_id, image_id, size: thumb\|full` | `file` | +| `list_artifacts` | read | `project_id` | `artifacts [{ path, name, size, modified, mime, title? }]` | +| `read_file` | read | `project_id \| bot_id, path, max_bytes?` | `file { name, mime, size, text? \| base64?, truncated }` | +| `list_permissions` | read | `bot_id?` | `permissions [PermissionRequest]` (pending only) | +| `answer_permission` | control | `request_id, decision: allow_once\|allow_session\|deny, reason?` | `permission` | + +| Push | Payload | +| --- | --- | +| `chat_turns` | `{ bot_id, turns }`: new or changed turns (the open one, mostly) | +| `permission_request` | `{ request }` | +| `permission_resolved` | `{ request_id, bot_id, outcome: allowed_once\|allowed_session\|denied\|expired\|answered_in_terminal }` | + +- **Paths.** `read_file` accepts only paths that resolve inside the bot's own + directory or its project's artifacts directory, using the same rule as + peer-bot artifact transfer. Text is returned as text and anything else as + base64, capped at 16 MiB. +- **Grants.** Answering a permission is `control`, the same grant as typing + the answer into the terminal. `approve` stays the authority to rule on + decisions. A device with `read` + `approve` (the phone) can rule on + decisions but cannot let a bot run a tool. + +### Live updates + +The daemon keeps a per-bot reader that tracks the transcript file and the +byte offset it has parsed, and re-parses only the open turn. It reads on the +hooks it already receives (`UserPromptSubmit`, `PostToolUse`, `Stop`, +`Notification`), plus a 1 s poll while a bot is `working`, and pushes +`chat_turns` when something changed. Claude Code writes each assistant message +when it completes, so text appears message by message and steps appear as +they finish. That is live enough without token streaming. + +The same reader replaces the sidebar's `activity.rs` scan. The preview line +becomes the open turn's current step or last text, which also fixes turns +that only used tools showing no preview. + +## Permissions + +The issue asks for a blocking `PreToolUse` hook. **We use Claude Code's +`PermissionRequest` hook instead**, which also blocks and decides the tool. +`PreToolUse` fires before every tool call, including the many the bot is +already allowed to run, so a blocking `PreToolUse` would either stall every +step behind the daemon or force the daemon to re-implement Claude Code's +permission rules to know which calls need asking. `PermissionRequest` fires +exactly when Claude Code would show its own prompt, which is the case the card +replaces. The hook, its allow/deny `behavior` and `updatedPermissions` are +present in the installed Claude Code (2.1.286). The implementation must still +confirm the exact input and output schema against that version before relying +on it. + +Flow: + +1. The hook posts its stdin (tool name, input, session, permission + suggestions) to `POST /hook/permission` with the bot's token, and waits. + The hook entry sets a `timeout` longer than the daemon's answer window. +2. The daemon records a `PermissionRequest { id, bot_id, tool, input, summary, + created_at, expires_at }`, sets the bot to `waiting_for_approval`, pushes + `permission_request`, raises a system notification, and holds the HTTP + request open. +3. The card offers **Allow once**, **Allow for this session** and **Deny** + (with an optional reason sent back to the bot). + - Allow once → `behavior: allow`. + - Allow for this session → `allow` plus the session-scoped rule Claude Code + suggested (`updatedPermissions`, destination `session`). It is never + written to a settings file. + - Deny → `behavior: deny` with the reason as the message. +4. If the request is not answered within the answer window (default 10 min, + `permission_timeout_minutes` in config), the daemon answers deny with + "The owner did not answer in time", resolves the card as `expired` and + the bot moves on. +5. If the daemon is unreachable or answers nothing, the hook prints nothing + and exits 0. Claude Code then shows its own prompt in the terminal, and the + chat shows "answer in the terminal". This is the permission-relay fallback, + and it is still closed: nothing is allowed without someone answering. + +The card is rendered inline in the open turn and pinned above the composer +until it is answered. The bot header, the sidebar dot and the toast all point +to it. + +**Done means proven live.** The issue stays open until a real Claude Code +session has shown that Allow from the card runs the tool without the terminal +prompt, Deny blocks it, and expiry denies it. A test runs this against the +installed `claude` binary; it is gated behind an env var because it is billed. + +## The desktop + +```text +┌ sidebar ┐┌ header: avatar · name · state · runtime · ⓘ ────────────────────┐ +│ ││ [ Chat ] [ Terminal ] [ Routines ] │ [ Info ] [ Files ] │ +│ bots ││ │ │ +│ ... ││ ┌ turn ──────────────────────────────┐ │ artifacts list │ +│ ││ │ Task from lead · 14:02 · took 3m │ │ ─ report.md │ +│ ││ │ "port the updater to Windows" │ │ ─ screenshot.png │ +│ ││ │ ▸ 6 steps · 3 edits +120 −4 · 2 🖼 │ │ │ +│ ││ │ [img][img] │ │ preview: │ +│ ││ │ Done — the updater builds on… │ │ rendered markdown, │ +│ ││ │ ✓ Completed task · report.md │ │ highlighted code, │ +│ ││ └─────────────────────────────────────┘ │ image, diff │ +│ ││ ┌ permission ─────────────────────────┐ │ │ +│ ││ │ Bash: rm -rf build/ [Allow once] │ │ │ +│ ││ │ [Allow for session] [Deny…] │ │ │ +│ ││ └─────────────────────────────────────┘ │ │ +│ ││ ┌ composer ──────────────────── ⏎ ┐ │ │ +└─────────┘└──────────────────────────────────────────────────────────────────┘ +``` + +- **Tabs.** Chat is the default and opens on the newest turn. Terminal stays + mounted so switching is instant, as it is today. The last tab per bot is + remembered. +- **Turns.** Each turn has a header naming its trigger ("You", "Task from + lead", "Routine nightly-report", "You, in the terminal"), the time and the + duration. Then come the trigger text, the items, and a stats line. + - Consecutive steps fold into a group ("6 steps · 3 edits +120 −4"), open by + default only while the turn is running or when it has four steps or fewer. + - Clicking a step loads its detail: command, output and a diff view with + added and removed lines. + - Images show as a thumbnail strip and open in a viewer. +- **Bubbles.** Your messages are right-aligned. The bot's text is + left-aligned, rendered as markdown with highlighted code blocks and a copy + button. Bus traffic and routine prompts are labelled asides ("From lead · + task", "To windev · task"), never styled as you. +- **Composer.** Enter sends through `send_user_message`; Shift+Enter adds a + newline. Each message shows its delivery state from `delivery_update` + (queued, delivered, failed with retry). The composer is disabled with a + reason when the connection is read-only or the bot is stopped. While the + bot works, the composer still sends: the message is queued into the running + turn, as the bus already does. +- **Files panel.** A second tab in the right panel lists the project's + artifacts, newest first, including files that arrived from peers. It + previews markdown, code with syntax highlighting, images and plain text. + Artifact paths in a `Completed` item, and file paths the bot mentions, + open here. +- **Linked bots** (from peer bots) have no local transcript. Their chat shows + their DM conversation from the bus (`list_messages`), labelled with the + machine, until phase 5 proxies their real chat over the peer link. +- **Markdown policy.** Bot output stays untrusted. Images render only from + daemon-served bytes (`read_file`, `get_chat_image`) and never from a URL in + the text. `http(s)` links open in the system browser after a hover shows the + full URL. Every other scheme stays text. +- **Keyboard.** + - ⌘1/⌘2/⌘3 switch between Chat, Terminal and Routines. + - ⌘L focuses the composer. + - In a pending permission card, `a` allows once, `s` allows for the session + and `d` denies, with the key hints visible. + - Esc returns focus to the turn list. + +## Beyond the issue's v1 + +The issue left four things out. Here is where each one lands: + +- **Answering decisions in the thread.** In phase 6. A `Decision` item shows + the options and answers through `answer_decision`, the same request the + Control Center uses. The Control Center stays the inbox and the record. +- **Token streaming.** Not planned. Message-by-message updates from the + transcript are what the runtimes give us without a different integration. +- **Parsing xterm into bubbles.** Rejected. The transcript is structured, + and scraping a screen is not. +- **Agent SDK.** Out of scope for the UI. It is a new runtime adapter and + deserves its own issue. The chat model above would serve it unchanged. + +Gaps the issue did not mention, and the phase that covers each: + +- **Codex bots** (phase 4). The worker already sees every App Server item. + `codex-observations.jsonl` will record tool items too (commands, file + changes with diffs) in the same turn shape. Approval requests become the + same permission cards, answered over the App Server. +- **Linked bots** (phase 5). The peer link gains a `chat` request, so a + bot on the other machine reads like a local one, and its artifacts are + readable through the link. +- **Attachments in the composer** (phase 6). Dropping a file copies it into + the project's artifacts directory and adds its path to the message. +- **Slash commands from the composer** (phase 6). A message starting with + `/` is typed into the terminal, with a hint that it runs there. +- **Search in chat** (phase 6), across the turns already loaded and the + bus search the daemon already has. +- **gravity_os** (phase 6). Moves from Gravity Lens to `list_chat`, + `get_chat_step`, `get_chat_image`, `list_artifacts` and `read_file`, and + Lens is retired. + +## Phases + +1. **Daemon chat model.** The transcript parser and reader, `list_chat`, + `get_chat_step`, `get_chat_image`, `chat_turns`, `list_artifacts`, + `read_file`, and the `chat` capability. Fixture transcripts with real + record shapes (typed, peer-delivered, tool results with images and + structured patches, compaction, interruptions). +2. **Desktop chat.** The Chat tab as default, turns, steps, detail, diffs, + images, the composer with delivery state, the Files panel with previews + and highlighting, and the linked-bot DM view. Ladle stories for every + item kind, with CI-generated baselines. +3. **Permissions.** The `PermissionRequest` hook, `/hook/permission`, cards, + `answer_permission`, expiry and the terminal fallback, plus the live + Claude Code check that closes the issue. +4. **Codex parity.** Tool items in observations and approvals answered from + cards. +5. **Linked bots over the peer link.** +6. **Beyond v1.** Decisions in the thread, attachments, slash commands, + search, and moving gravity_os off Lens. + +## Found while researching + +These are not part of this work, but they surfaced while mapping the code: + +- On Unix the Stop hook posts a fixed body, so `transcript_path` never + reaches the daemon for Claude bots. Separately, + `routine_run_id_from_transcript` expects the record to start with + `[routine "`, but Claude Code prefixes delivered messages with "Another + Claude session sent a message:". Routine completion via the transcript + may therefore never match for Claude bots on Unix. Phase 1's parser + replaces that scan. +- `transcript_dir` replaces `/` and `.` but not `_`, while bot directory + names may contain `_`. If Claude Code replaces every non-alphanumeric + character, bots with `_` in their path have no activity preview. Phase 1 + fixes the mangling. +- The desktop's `list_messages` type sends `before_id`, but the daemon reads + `before_num`. +- Claude Code tells the bot that a delivered message "came from another + Claude session — not typed by your user". The composer's messages arrive + that way. The system prompt already overrides this for Gravity, but if + bots treat composer messages as less authoritative in practice, the + composer can type into the session instead when the bot is idle. That + would be a phase 2 follow-up, decided from live use. From 6ca3cea4fcb98653a60206762d800324ff556c66 Mon Sep 17 00:00:00 2001 From: Manuel Rinaldi Date: Thu, 1 Oct 2026 19:13:03 +0300 Subject: [PATCH 16/55] feat(chat): serve bots' transcripts as chat turns Phase 1 of the chat pane (ahilles107/gravity#13). The daemon now reads a bot's runtime transcript and serves it as turns: what woke the bot (the owner from the composer or the terminal, another bot, a routine, a ruling, a background job), its text, its tool steps with diff counts and images, and Gravity's own bus calls as sent messages, results and decisions. A per-bot reader folds the transcript in incrementally and a watcher pushes chat_turns for loaded chats as they grow. Step detail, images and files are fetched on demand; read_file only reaches the bot's own directory and its project's artifacts. New read-grant requests list_chat, get_chat_step, get_chat_image, list_artifacts and read_file, behind a `chat` capability. Also fixes two bugs the research turned up: - transcript_dir kept `_`, while Claude Code files every non-alphanumeric character as `-`, so bots under such paths had no activity preview. - Routine runs never completed from the transcript on Unix: the Stop hook did not forward Claude Code's payload (so no transcript_path), and the run-id scan did not expect Claude Code's peer-message wrapper. Co-Authored-By: Claude Opus 5.5 --- crates/gravityd/src/activity.rs | 12 +- crates/gravityd/src/activity/tests.rs | 9 + crates/gravityd/src/app.rs | 4 + crates/gravityd/src/chat.rs | 49 ++ crates/gravityd/src/chat/builder.rs | 505 ++++++++++++++++++ crates/gravityd/src/chat/builder_tests.rs | 267 +++++++++ crates/gravityd/src/chat/envelope.rs | 137 +++++ crates/gravityd/src/chat/files.rs | 206 +++++++ crates/gravityd/src/chat/model.rs | 173 ++++++ crates/gravityd/src/chat/steps.rs | 269 ++++++++++ crates/gravityd/src/chat/store.rs | 288 ++++++++++ crates/gravityd/src/events.rs | 5 + crates/gravityd/src/lib.rs | 1 + crates/gravityd/src/paths.rs | 40 +- crates/gravityd/src/scheduler/dispatch.rs | 21 + crates/gravityd/src/server.rs | 1 + crates/gravityd/src/ws.rs | 1 + crates/gravityd/src/ws/chat.rs | 138 +++++ crates/gravityd/src/ws/dispatch.rs | 10 + crates/gravityd/tests/chat.rs | 184 +++++++ docs/protocol.md | 9 + .../specs/2026-09-16-chat-pane-design.md | 3 +- 22 files changed, 2305 insertions(+), 27 deletions(-) create mode 100644 crates/gravityd/src/chat.rs create mode 100644 crates/gravityd/src/chat/builder.rs create mode 100644 crates/gravityd/src/chat/builder_tests.rs create mode 100644 crates/gravityd/src/chat/envelope.rs create mode 100644 crates/gravityd/src/chat/files.rs create mode 100644 crates/gravityd/src/chat/model.rs create mode 100644 crates/gravityd/src/chat/steps.rs create mode 100644 crates/gravityd/src/chat/store.rs create mode 100644 crates/gravityd/src/ws/chat.rs create mode 100644 crates/gravityd/tests/chat.rs diff --git a/crates/gravityd/src/activity.rs b/crates/gravityd/src/activity.rs index bb70464..ae7e6e3 100644 --- a/crates/gravityd/src/activity.rs +++ b/crates/gravityd/src/activity.rs @@ -68,18 +68,14 @@ const TAIL_BYTES: u64 = 1 << 20; const MAX_CHARS: usize = 200; /// Claude Code's transcript directory for a workspace: the absolute path with -/// every `/` and `.` replaced by `-`, under `~/.claude/projects`. +/// every character that is not an ASCII letter or digit replaced by `-`, under +/// `~/.claude/projects`. Underscores count: a workspace under `my_app` is +/// filed as `my-app`. pub(crate) fn transcript_dir(home: &Path, workspace: &Path) -> PathBuf { let mangled: String = workspace .to_string_lossy() .chars() - .map(|c| { - if c == '/' || c == '.' || cfg!(windows) && (c == '\\' || c == ':') { - '-' - } else { - c - } - }) + .map(|c| if c.is_ascii_alphanumeric() { c } else { '-' }) .collect(); home.join(".claude").join("projects").join(mangled) } diff --git a/crates/gravityd/src/activity/tests.rs b/crates/gravityd/src/activity/tests.rs index 76f8dc0..5410381 100644 --- a/crates/gravityd/src/activity/tests.rs +++ b/crates/gravityd/src/activity/tests.rs @@ -36,6 +36,15 @@ fn mangles_the_workspace_path_the_way_claude_code_does() { ); } +#[test] +fn mangles_underscores_like_claude_code() { + let dir = transcript_dir( + Path::new("/Users/me"), + Path::new("/Users/me/.gravity/projects/my_app/bots/q_a/workspace"), + ); + assert!(dir.ends_with("-Users-me--gravity-projects-my-app-bots-q-a-workspace")); +} + #[test] fn reads_the_newest_assistant_turn() { let tmp = tempfile::tempdir().expect("tmp"); diff --git a/crates/gravityd/src/app.rs b/crates/gravityd/src/app.rs index 5713890..a98c3a9 100644 --- a/crates/gravityd/src/app.rs +++ b/crates/gravityd/src/app.rs @@ -28,6 +28,7 @@ pub const CAPABILITIES: &[&str] = &[ "config", "decisions", "bot_runtime", + "chat", ]; pub struct AppState { @@ -42,6 +43,8 @@ pub struct AppState { /// Live links to peer daemons, shared by the delivery worker (forwarding /// to linked bots) and the control plane (pairing and linking). pub peers: crate::peer::PeerHub, + /// Bots' conversations read from their transcripts, for the chat pane. + pub chat: crate::chat::ChatStore, pub started_at: Instant, /// Wall-clock start, kept alongside the monotonic `started_at` purely so /// [`AppState::stale_build`] can compare it against a file mtime. @@ -79,6 +82,7 @@ impl AppState { supervisor, auto_compact, peers: crate::peer::PeerHub::default(), + chat: crate::chat::ChatStore::default(), started_at: Instant::now(), started_wall: SystemTime::now(), })) diff --git a/crates/gravityd/src/chat.rs b/crates/gravityd/src/chat.rs new file mode 100644 index 0000000..2f567f1 --- /dev/null +++ b/crates/gravityd/src/chat.rs @@ -0,0 +1,49 @@ +//! A bot's conversation read from its runtime's transcript, for the chat +//! pane. See `docs/superpowers/specs/2026-09-16-chat-pane-design.md`. + +use std::sync::Arc; +use std::time::Duration; + +use crate::app::AppState; +use crate::events::Push; + +mod builder; +#[cfg(test)] +mod builder_tests; +mod envelope; +pub mod files; +pub mod model; +mod steps; +mod store; + +pub(crate) use envelope::unwrap_peer; +pub use store::ChatStore; + +/// How often the watcher looks for transcript growth in loaded chats. +const WATCH_INTERVAL: Duration = Duration::from_secs(1); + +/// Keeps every loaded chat current and pushes the turns that change, so an +/// open chat follows the bot as it works. +pub async fn watch(app: Arc) { + let mut tick = tokio::time::interval(WATCH_INTERVAL); + tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + loop { + tick.tick().await; + let app = app.clone(); + let _ = tokio::task::spawn_blocking(move || { + for bot_id in app.chat.loaded() { + let Ok(Some(bot)) = app.db.get_live_bot(&bot_id) else { + continue; + }; + match app.chat.refresh(&app, &bot) { + Ok(turns) if !turns.is_empty() => { + app.events.push(Push::ChatTurns { bot_id, turns }); + } + Ok(_) => {} + Err(e) => tracing::debug!(bot_id, error = %e, "chat refresh failed"), + } + } + }) + .await; + } +} diff --git a/crates/gravityd/src/chat/builder.rs b/crates/gravityd/src/chat/builder.rs new file mode 100644 index 0000000..0f2f7f9 --- /dev/null +++ b/crates/gravityd/src/chat/builder.rs @@ -0,0 +1,505 @@ +//! Folding transcript records into turns. Lines arrive in file order, once +//! each, so a live session is read incrementally: a new line extends the open +//! turn or starts the next one. Heavy content (tool input and output, image +//! bytes) is not kept; the builder records where it lives in the file. + +use std::collections::{BTreeSet, HashMap}; + +use chrono::{DateTime, Utc}; +use serde_json::Value; + +use super::envelope::{self, Delivered}; +use super::model::{AsideKind, ChatItem, ChatTurn, ImageRef, OwnerVia, Step, StepStatus, Trigger}; +use super::steps::{self, BusCall, ToolKind}; + +/// Longest text kept in a turn. A bot that pastes a whole file into its +/// answer is still readable, and the rest is in the terminal. +const MAX_TEXT_CHARS: usize = 20_000; +const MAX_TRIGGER_CHARS: usize = 4_000; + +/// Where a step's heavy content lives in the transcript. +#[derive(Debug, Clone)] +pub struct StepLocator { + pub use_offset: u64, + pub result_offset: Option, +} + +/// Where an image's bytes live: the line, the tool_result block, and the +/// image block inside it. +#[derive(Debug, Clone, Copy)] +pub struct ImageLocator { + pub offset: u64, + pub block: usize, + pub inner: usize, +} + +pub struct Builder { + bot_id: String, + /// Upper-case envelope names to the bots' real names. + names: HashMap, + pub turns: Vec, + /// tool_use id → (turn, item). + calls: HashMap, + pub steps: HashMap, + pub images: HashMap, + changed: BTreeSet, + last_at: DateTime, +} + +impl Builder { + pub fn new(bot_id: &str, names: HashMap) -> Self { + Self { + bot_id: bot_id.to_string(), + names, + turns: Vec::new(), + calls: HashMap::new(), + steps: HashMap::new(), + images: HashMap::new(), + changed: BTreeSet::new(), + last_at: DateTime::::MIN_UTC, + } + } + + /// The turns that changed since the last call. + pub fn take_changed(&mut self) -> Vec { + std::mem::take(&mut self.changed) + .into_iter() + .filter_map(|i| self.turns.get(i).cloned()) + .collect() + } + + pub fn push_line(&mut self, offset: u64, line: &str) { + let Ok(record) = serde_json::from_str::(line) else { + return; + }; + if record.get("isSidechain").and_then(Value::as_bool) == Some(true) { + return; + } + let at = record + .get("timestamp") + .and_then(Value::as_str) + .and_then(|t| DateTime::parse_from_rfc3339(t).ok()) + .map(|t| t.with_timezone(&Utc)) + .unwrap_or(self.last_at); + self.last_at = at; + let id = record + .get("uuid") + .and_then(Value::as_str) + .map(str::to_string) + .unwrap_or_else(|| format!("o{offset}")); + match record.get("type").and_then(Value::as_str) { + Some("user") => self.user(offset, &id, at, &record), + Some("assistant") => self.assistant(offset, &id, at, &record), + Some("system") => self.system(&id, at, &record), + Some("attachment") => self.attachment(&id, at, &record), + _ => {} + } + } + + fn user(&mut self, offset: u64, id: &str, at: DateTime, record: &Value) { + let content = &record["message"]["content"]; + if let Some(text) = content.as_str() { + self.prompt(id, at, record, text); + return; + } + let Some(blocks) = content.as_array() else { + return; + }; + let mut text = String::new(); + for (index, block) in blocks.iter().enumerate() { + match block.get("type").and_then(Value::as_str) { + Some("tool_result") => self.result(offset, index, block, record), + Some("text") => text.push_str(block["text"].as_str().unwrap_or_default()), + _ => {} + } + } + if text.starts_with("[Request interrupted") { + let turn = self.current(at); + self.aside(turn, id, AsideKind::Interrupted, "Interrupted".to_string()); + } else if !text.is_empty() && !blocks.iter().any(|b| b["type"] == "tool_result") { + // A prompt written as text blocks: Codex observations, or input + // with pasted images. + self.prompt(id, at, record, &text); + } + } + + /// A user record that starts a turn, unless it is runtime bookkeeping. + fn prompt(&mut self, id: &str, at: DateTime, record: &Value, text: &str) { + let flag = |key: &str| record.get(key).and_then(Value::as_bool) == Some(true); + if flag("isCompactSummary") || flag("isVisibleInTranscriptOnly") { + return; + } + let origin = record["origin"]["kind"].as_str(); + let trigger = + if origin == Some("task-notification") || text.starts_with("") { + Trigger::Background { + text: tag(text, "summary") + .unwrap_or("A background task finished") + .to_string(), + } + } else if record.get("scheduledTaskId").is_some() { + Trigger::Background { + text: format!("Scheduled wake-up: {}", first_line(text)), + } + } else if let Some(delivered) = envelope::parse(text) { + self.delivered(delivered) + } else if origin == Some("human") { + Trigger::Owner { + text: owner_text(text), + via: OwnerVia::Terminal, + } + } else if text.starts_with(" Trigger { + match delivered { + Delivered::Message { + num, + from, + kind, + task_id, + body, + } => { + let body = steps::truncate(&body, MAX_TRIGGER_CHARS); + if from == "USER" && kind == "chat" { + Trigger::Owner { + text: body, + via: OwnerVia::Chat, + } + } else { + // `USER` on anything but a chat is the daemon speaking: + // introductions, expiries, renames. + let from = if from == "USER" { + "Gravity".to_string() + } else { + self.names.get(&from).cloned().unwrap_or(from) + }; + Trigger::Bus { + from, + msg_kind: kind, + num, + text: body, + task_id, + } + } + } + Delivered::Routine { + name, + run_id, + prompt, + } => Trigger::Routine { + name, + text: steps::truncate(&prompt, MAX_TRIGGER_CHARS), + run_id, + }, + Delivered::Decision { decision_id, text } => Trigger::Ruling { + decision_id, + text: steps::truncate(&text, MAX_TRIGGER_CHARS), + }, + } + } + + fn assistant(&mut self, offset: u64, id: &str, at: DateTime, record: &Value) { + let Some(blocks) = record["message"]["content"].as_array() else { + return; + }; + let turn = self.current(at); + for (index, block) in blocks.iter().enumerate() { + match block.get("type").and_then(Value::as_str) { + Some("text") => { + let text = block["text"].as_str().unwrap_or_default().trim(); + if !text.is_empty() { + self.text(turn, &format!("{id}:{index}"), text); + } + } + Some("tool_use") => self.call(turn, offset, block), + _ => {} + } + } + } + + fn text(&mut self, turn: usize, id: &str, text: &str) { + let items = &mut self.turns[turn].items; + if let Some(ChatItem::Text { markdown, .. }) = items.last_mut() { + markdown.push_str("\n\n"); + markdown.push_str(text); + *markdown = steps::truncate(markdown, MAX_TEXT_CHARS); + } else { + items.push(ChatItem::Text { + id: id.to_string(), + markdown: steps::truncate(text, MAX_TEXT_CHARS), + }); + } + self.changed.insert(turn); + } + + fn call(&mut self, turn: usize, offset: u64, block: &Value) { + let id = block["id"].as_str().unwrap_or_default().to_string(); + let name = block["name"].as_str().unwrap_or_default(); + let input = &block["input"]; + let stats = &mut self.turns[turn].stats; + let item = match steps::bus_call(name, input) { + Some(BusCall::Sent { to, kind, body }) => { + stats.sent += 1; + ChatItem::Sent { + id: id.clone(), + to, + msg_kind: kind, + body: steps::truncate(&body, MAX_TRIGGER_CHARS), + } + } + Some(BusCall::Completed { + task_id, + result, + artifacts, + }) => ChatItem::Completed { + id: id.clone(), + task_id, + result: steps::truncate(&result, MAX_TEXT_CHARS), + artifacts, + }, + Some(BusCall::Decision { title }) => ChatItem::Decision { + id: id.clone(), + decision_id: None, + title, + }, + None => { + let described = steps::describe(name, input); + match described.kind { + ToolKind::Command => stats.commands += 1, + ToolKind::Read => stats.reads += 1, + ToolKind::Edit => stats.edits += 1, + ToolKind::Other => {} + } + ChatItem::Step(Step { + id: id.clone(), + tool: name.to_string(), + title: described.title, + subtitle: described.subtitle, + status: StepStatus::Running, + minor: described.minor, + added: None, + removed: None, + images: Vec::new(), + }) + } + }; + let items = &mut self.turns[turn].items; + self.calls.insert(id.clone(), (turn, items.len())); + items.push(item); + self.steps.insert( + id, + StepLocator { + use_offset: offset, + result_offset: None, + }, + ); + self.changed.insert(turn); + } + + fn result(&mut self, offset: u64, index: usize, block: &Value, record: &Value) { + let use_id = block["tool_use_id"].as_str().unwrap_or_default(); + let Some(&(turn, item)) = self.calls.get(use_id) else { + return; + }; + if let Some(locator) = self.steps.get_mut(use_id) { + locator.result_offset = Some(offset); + } + let failed = block.get("is_error").and_then(Value::as_bool) == Some(true); + let mut images = Vec::new(); + for (inner, part) in block["content"] + .as_array() + .into_iter() + .flatten() + .enumerate() + { + if part["type"] == "image" { + let image_id = format!("{offset}:{index}:{inner}"); + let mime = part["source"]["media_type"].as_str().unwrap_or("image/png"); + self.images.insert( + image_id.clone(), + ImageLocator { + offset, + block: index, + inner, + }, + ); + images.push(ImageRef { + id: image_id, + mime: mime.to_string(), + }); + } + } + let (added, removed) = patch_counts(&record["toolUseResult"]); + let turn_ref = &mut self.turns[turn]; + turn_ref.stats.images += images.len() as u32; + turn_ref.stats.added += added; + turn_ref.stats.removed += removed; + if failed { + turn_ref.stats.errors += 1; + } + match &mut turn_ref.items[item] { + ChatItem::Step(step) => { + step.status = if failed { + StepStatus::Error + } else { + StepStatus::Ok + }; + step.images.extend(images); + if added + removed > 0 { + step.added = Some(added); + step.removed = Some(removed); + } + } + ChatItem::Decision { decision_id, .. } => { + *decision_id = result_text(block) + .and_then(|text| serde_json::from_str::(&text).ok()) + .and_then(|v| v["id"].as_str().map(str::to_string)); + } + _ => {} + } + self.changed.insert(turn); + } + + fn system(&mut self, id: &str, at: DateTime, record: &Value) { + match record.get("subtype").and_then(Value::as_str) { + Some("turn_duration") => { + if let Some(turn) = self.turns.last_mut().filter(|t| t.open) { + turn.open = false; + turn.ended_at = Some(at); + turn.duration_ms = record.get("durationMs").and_then(Value::as_i64); + self.changed.insert(self.turns.len() - 1); + } + } + Some("compact_boundary") => { + let turn = self.current(at); + self.aside( + turn, + id, + AsideKind::Compacted, + "Conversation compacted".to_string(), + ); + } + _ => {} + } + } + + fn attachment(&mut self, id: &str, at: DateTime, record: &Value) { + let attachment = &record["attachment"]; + if attachment["type"] != "queued_command" { + return; + } + let prompt = attachment["prompt"].as_str().unwrap_or_default(); + let text = match envelope::parse(prompt) { + Some(Delivered::Message { + from, kind, body, .. + }) => { + let from = self.names.get(&from).cloned().unwrap_or(from); + format!("From {from} · {kind}: {}", steps::truncate(&body, 600)) + } + _ => steps::truncate(prompt, 600), + }; + let turn = self.current(at); + self.aside(turn, id, AsideKind::Incoming, text); + } + + fn aside(&mut self, turn: usize, id: &str, kind: AsideKind, text: String) { + self.turns[turn].items.push(ChatItem::Aside { + id: id.to_string(), + kind, + text, + }); + self.changed.insert(turn); + } + + fn start(&mut self, id: &str, at: DateTime, trigger: Trigger) { + if let Some(previous) = self.turns.last_mut().filter(|t| t.open) { + previous.open = false; + self.changed.insert(self.turns.len() - 1); + } + self.turns.push(ChatTurn { + id: id.to_string(), + bot_id: self.bot_id.clone(), + started_at: at, + ended_at: None, + duration_ms: None, + open: true, + trigger, + items: Vec::new(), + stats: Default::default(), + }); + self.changed.insert(self.turns.len() - 1); + } + + /// The turn new content belongs to: the open one, or a resumed turn when + /// the transcript picks up mid-conversation. + fn current(&mut self, at: DateTime) -> usize { + if !self.turns.last().is_some_and(|t| t.open) { + let id = format!("resumed-{}", at.timestamp_millis()); + self.start(&id, at, Trigger::Resumed); + } + self.turns.len() - 1 + } +} + +/// Lines added and removed by an edit, from Claude Code's structured patch. +pub fn patch_counts(result: &Value) -> (u32, u32) { + let mut added = 0; + let mut removed = 0; + for hunk in result["structuredPatch"].as_array().into_iter().flatten() { + for line in hunk["lines"].as_array().into_iter().flatten() { + match line.as_str().and_then(|l| l.chars().next()) { + Some('+') => added += 1, + Some('-') => removed += 1, + _ => {} + } + } + } + (added, removed) +} + +/// A tool result's text, whether it is a string or text blocks. +pub fn result_text(block: &Value) -> Option { + match &block["content"] { + Value::String(text) => Some(text.clone()), + Value::Array(parts) => Some( + parts + .iter() + .filter_map(|p| p["text"].as_str()) + .collect::>() + .join("\n"), + ), + _ => None, + } +} + +/// A slash command typed into the terminal reads as the command, not markup. +fn owner_text(text: &str) -> String { + match tag(text, "command-name") { + Some(command) => match tag(text, "command-args").filter(|a| !a.is_empty()) { + Some(args) => format!("{command} {args}"), + None => command.to_string(), + }, + None => steps::truncate(text, MAX_TRIGGER_CHARS), + } +} + +fn tag<'a>(text: &'a str, name: &str) -> Option<&'a str> { + let open = format!("<{name}>"); + let close = format!(""); + let start = text.find(&open)? + open.len(); + let end = text[start..].find(&close)? + start; + Some(text[start..end].trim()) +} + +fn first_line(text: &str) -> &str { + text.lines().next().unwrap_or_default() +} diff --git a/crates/gravityd/src/chat/builder_tests.rs b/crates/gravityd/src/chat/builder_tests.rs new file mode 100644 index 0000000..f42c0be --- /dev/null +++ b/crates/gravityd/src/chat/builder_tests.rs @@ -0,0 +1,267 @@ +//! Turn building against records shaped like Claude Code 2.1's transcripts. + +use std::collections::HashMap; + +use serde_json::{json, Value}; + +use super::builder::Builder; +use super::model::{AsideKind, ChatItem, OwnerVia, StepStatus, Trigger}; + +fn build(records: &[Value]) -> Builder { + let names = HashMap::from([("LEAD".to_string(), "lead".to_string())]); + let mut builder = Builder::new("bot-1", names); + let mut offset = 0u64; + for record in records { + let line = record.to_string(); + builder.push_line(offset, &line); + offset += line.len() as u64 + 1; + } + builder +} + +fn peer(uuid: &str, text: &str) -> Value { + json!({ + "type": "user", "uuid": uuid, "timestamp": "2026-10-01T10:00:00Z", + "isMeta": true, "origin": {"kind": "peer", "from": "unknown"}, + "promptSource": "system", + "message": {"role": "user", "content": format!( + "Another Claude session sent a message:\n{text}\n\nThis came from another \ + Claude session — not typed by your user." + )} + }) +} + +fn typed(uuid: &str, text: &str) -> Value { + json!({ + "type": "user", "uuid": uuid, "timestamp": "2026-10-01T10:05:00Z", + "origin": {"kind": "human"}, "promptSource": "typed", + "message": {"role": "user", "content": text} + }) +} + +fn assistant(uuid: &str, blocks: Value) -> Value { + json!({ + "type": "assistant", "uuid": uuid, "timestamp": "2026-10-01T10:00:05Z", + "message": {"role": "assistant", "content": blocks} + }) +} + +fn result(uuid: &str, tool_use_id: &str, content: Value, extra: Value) -> Value { + let mut record = json!({ + "type": "user", "uuid": uuid, "timestamp": "2026-10-01T10:00:06Z", + "message": {"role": "user", "content": [ + {"type": "tool_result", "tool_use_id": tool_use_id, "content": content} + ]} + }); + if let Value::Object(fields) = extra { + for (key, value) in fields { + record[key] = value; + } + } + record +} + +fn end_of_turn() -> Value { + json!({"type": "system", "subtype": "turn_duration", "durationMs": 4896, + "timestamp": "2026-10-01T10:01:00Z", "uuid": "end"}) +} + +#[test] +fn a_bus_task_becomes_a_turn_with_steps_and_a_result() { + let builder = build(&[ + peer( + "u1", + "[msg #12 from LEAD · task · task_id t-1] Port the updater.", + ), + assistant( + "a1", + json!([ + {"type": "thinking", "thinking": "hmm"}, + {"type": "text", "text": "On it."}, + {"type": "tool_use", "id": "tool-1", "name": "Edit", + "input": {"file_path": "/w/src/update.rs", "old_string": "a", "new_string": "b"}} + ]), + ), + result( + "r1", + "tool-1", + json!("ok"), + json!({"toolUseResult": {"structuredPatch": [ + {"oldStart": 1, "oldLines": 1, "newStart": 1, "newLines": 2, + "lines": ["-a", "+b", "+c"]} + ]}}), + ), + assistant( + "a2", + json!([ + {"type": "tool_use", "id": "tool-2", "name": "mcp__gravity-bus__complete_task", + "input": {"task_id": "t-1", "result": "Ported.", "artifacts": ["/w/report.md"]}} + ]), + ), + end_of_turn(), + ]); + assert_eq!(builder.turns.len(), 1); + let turn = &builder.turns[0]; + assert!(!turn.open); + assert_eq!(turn.duration_ms, Some(4896)); + assert_eq!( + turn.trigger, + Trigger::Bus { + from: "lead".to_string(), + msg_kind: "task".to_string(), + num: 12, + text: "Port the updater.".to_string(), + task_id: Some("t-1".to_string()), + } + ); + assert!(matches!(&turn.items[0], ChatItem::Text { markdown, .. } if markdown == "On it.")); + match &turn.items[1] { + ChatItem::Step(step) => { + assert_eq!(step.title, "Edited update.rs"); + assert_eq!(step.status, StepStatus::Ok); + assert_eq!((step.added, step.removed), (Some(2), Some(1))); + } + other => panic!("expected a step, got {other:?}"), + } + assert!( + matches!(&turn.items[2], ChatItem::Completed { task_id, artifacts, .. } + if task_id == "t-1" && artifacts[0].name == "report.md") + ); + assert_eq!( + (turn.stats.edits, turn.stats.added, turn.stats.removed), + (1, 2, 1) + ); + assert!(builder.steps.contains_key("tool-1")); +} + +#[test] +fn the_owner_is_recognised_from_the_composer_and_the_terminal() { + let builder = build(&[ + peer("u1", "[msg #3 from USER · chat] Which SDK are you on?"), + assistant("a1", json!([{"type": "text", "text": "Windows 11 SDK."}])), + end_of_turn(), + typed( + "u2", + "/compact\n", + ), + ]); + assert_eq!( + builder.turns[0].trigger, + Trigger::Owner { + text: "Which SDK are you on?".to_string(), + via: OwnerVia::Chat + } + ); + assert_eq!( + builder.turns[1].trigger, + Trigger::Owner { + text: "/compact".to_string(), + via: OwnerVia::Terminal + } + ); + assert!(builder.turns[1].open); +} + +#[test] +fn the_daemon_and_routines_are_not_the_owner() { + let builder = build(&[ + peer( + "u1", + "[msg #1 from USER · note] You have just been created.", + ), + end_of_turn(), + peer("u2", "[routine \"nightly\" #4 · run_id r-1] /report"), + ]); + assert!(matches!(&builder.turns[0].trigger, Trigger::Bus { from, .. } if from == "Gravity")); + assert!( + matches!(&builder.turns[1].trigger, Trigger::Routine { name, run_id, .. } + if name == "nightly" && run_id.as_deref() == Some("r-1")) + ); +} + +#[test] +fn images_failures_and_asides_are_kept() { + let builder = build(&[ + typed("u1", "take a screenshot"), + assistant( + "a1", + json!([ + {"type": "tool_use", "id": "shot", "name": "mcp__chrome__screenshot", "input": {}}, + {"type": "tool_use", "id": "bad", "name": "Bash", "input": {"command": "false"}} + ]), + ), + result( + "r1", + "shot", + json!([ + {"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": "AAAA"}}, + {"type": "text", "text": "Screenshot."} + ]), + json!({}), + ), + json!({"type": "user", "uuid": "r2", "timestamp": "2026-10-01T10:00:07Z", + "message": {"content": [{"type": "tool_result", "tool_use_id": "bad", + "is_error": true, "content": "exit 1"}]}}), + json!({"type": "attachment", "uuid": "q1", "timestamp": "2026-10-01T10:00:08Z", + "attachment": {"type": "queued_command", + "prompt": "[msg #9 from LEAD · note] FYI the build is green"}}), + json!({"type": "user", "uuid": "i1", "timestamp": "2026-10-01T10:00:09Z", + "message": {"content": [{"type": "text", "text": "[Request interrupted by user]"}]}}), + json!({"type": "assistant", "uuid": "side", "isSidechain": true, + "message": {"content": [{"type": "text", "text": "sub-agent chatter"}]}}), + ]); + let turn = &builder.turns[0]; + let ChatItem::Step(shot) = &turn.items[0] else { + panic!("expected a step"); + }; + assert_eq!(shot.images.len(), 1); + assert!(builder.images.contains_key(&shot.images[0].id)); + assert!(matches!(&turn.items[1], ChatItem::Step(s) if s.status == StepStatus::Error)); + assert!( + matches!(&turn.items[2], ChatItem::Aside { kind: AsideKind::Incoming, text, .. } + if text == "From lead · note: FYI the build is green") + ); + assert!(matches!( + &turn.items[3], + ChatItem::Aside { + kind: AsideKind::Interrupted, + .. + } + )); + assert_eq!(turn.items.len(), 4, "sidechain records are skipped"); + assert_eq!((turn.stats.images, turn.stats.errors), (1, 1)); +} + +#[test] +fn codex_observations_read_as_turns() { + let builder = build(&[ + json!({"type": "user", "timestamp": "2026-10-01T10:00:00Z", + "message": {"content": [{"type": "text", "text": "[msg #5 from USER · chat] hello"}]}}), + json!({"type": "assistant", "timestamp": "2026-10-01T10:00:02Z", + "message": {"content": [{"type": "text", "text": "Hi there."}]}}), + ]); + assert_eq!(builder.turns.len(), 1); + assert!(matches!( + &builder.turns[0].trigger, + Trigger::Owner { + via: OwnerVia::Chat, + .. + } + )); + assert!( + builder.turns[0].id.starts_with('o'), + "ids fall back to the line offset" + ); +} + +#[test] +fn changed_turns_are_reported_once() { + let mut builder = build(&[typed("u1", "hi")]); + assert_eq!(builder.take_changed().len(), 1); + assert!(builder.take_changed().is_empty()); + builder.push_line( + 999, + &assistant("a1", json!([{"type": "text", "text": "hello"}])).to_string(), + ); + assert_eq!(builder.take_changed()[0].id, "u1"); +} diff --git a/crates/gravityd/src/chat/envelope.rs b/crates/gravityd/src/chat/envelope.rs new file mode 100644 index 0000000..d8e41a0 --- /dev/null +++ b/crates/gravityd/src/chat/envelope.rs @@ -0,0 +1,137 @@ +//! Reading back the envelopes the daemon delivers. A delivered message lands +//! in the transcript as user input; this turns it back into who sent what. +//! The formats are the ones `bus::envelope` renders. + +/// What Claude Code wraps around a message delivered over its inbox socket. +const PEER_PREFIX: &str = "Another Claude session sent a message:\n"; +/// The start of the trailer Claude Code appends after it. +const PEER_TRAILER: &str = "\n\nThis came from another Claude session"; + +#[derive(Debug, PartialEq, Eq)] +pub enum Delivered { + Message { + num: i64, + /// The sender as the envelope names it: `USER`, or a bot name in + /// upper case. + from: String, + kind: String, + task_id: Option, + body: String, + }, + Routine { + name: String, + run_id: Option, + prompt: String, + }, + Decision { + decision_id: String, + text: String, + }, +} + +/// The delivered text inside Claude Code's peer-message wrapper, or the text +/// itself when it is not wrapped (Codex observations, older transcripts). +pub fn unwrap_peer(text: &str) -> &str { + let inner = text.strip_prefix(PEER_PREFIX).unwrap_or(text); + match inner.find(PEER_TRAILER) { + Some(end) => &inner[..end], + None => inner, + } +} + +pub fn parse(text: &str) -> Option { + let text = unwrap_peer(text).trim_start(); + if let Some(rest) = text.strip_prefix("[msg #") { + return parse_message(rest); + } + if let Some(rest) = text.strip_prefix("[routine \"") { + return parse_routine(rest); + } + if let Some(rest) = text.strip_prefix("[decision ") { + let decision_id = rest.split_whitespace().next()?.to_string(); + return Some(Delivered::Decision { + decision_id, + text: text.to_string(), + }); + } + None +} + +/// `N from NAME · kind[ · re #M][ · task_id ID]] body` +fn parse_message(rest: &str) -> Option { + let (head, body) = rest.split_once(']')?; + let mut parts = head.split(" · "); + let (num, from) = parts.next()?.split_once(" from ")?; + let kind = parts.next()?.trim().to_string(); + let mut task_id = None; + for part in parts { + if let Some(id) = part.strip_prefix("task_id ") { + task_id = Some(id.trim().to_string()); + } + } + Some(Delivered::Message { + num: num.trim().parse().ok()?, + from: from.trim().to_string(), + kind, + task_id, + body: body.strip_prefix(' ').unwrap_or(body).to_string(), + }) +} + +/// `name" #N[ · run_id ID]] prompt` +fn parse_routine(rest: &str) -> Option { + let (name, rest) = rest.split_once('"')?; + let (head, prompt) = rest.split_once(']')?; + let run_id = head + .split(" · ") + .find_map(|part| part.strip_prefix("run_id ")) + .map(|id| id.trim().to_string()); + Some(Delivered::Routine { + name: name.to_string(), + run_id, + prompt: prompt.strip_prefix(' ').unwrap_or(prompt).to_string(), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn reads_a_wrapped_task_with_its_id() { + let text = "Another Claude session sent a message:\n[msg #12 from LEAD · task · re #3 \ + · task_id t-1] Port the updater.\nLine two.\n\nThis came from another \ + Claude session — not typed by your user."; + assert_eq!( + parse(text), + Some(Delivered::Message { + num: 12, + from: "LEAD".to_string(), + kind: "task".to_string(), + task_id: Some("t-1".to_string()), + body: "Port the updater.\nLine two.".to_string(), + }) + ); + } + + #[test] + fn reads_the_owner_chat_and_a_routine() { + assert!(matches!( + parse("[msg #4 from USER · chat] hi"), + Some(Delivered::Message { from, kind, .. }) if from == "USER" && kind == "chat" + )); + assert_eq!( + parse("[routine \"nightly\" #7 · run_id r-9] /report"), + Some(Delivered::Routine { + name: "nightly".to_string(), + run_id: Some("r-9".to_string()), + prompt: "/report".to_string(), + }) + ); + } + + #[test] + fn plain_text_is_not_an_envelope() { + assert_eq!(parse("just typed this"), None); + } +} diff --git a/crates/gravityd/src/chat/files.rs b/crates/gravityd/src/chat/files.rs new file mode 100644 index 0000000..768d995 --- /dev/null +++ b/crates/gravityd/src/chat/files.rs @@ -0,0 +1,206 @@ +//! Files a client may read: the project's shared artifacts, and anything +//! under a bot's own directory. Nothing else on the machine is reachable, +//! however the path is spelled. + +use std::path::{Path, PathBuf}; + +use base64::Engine; +use bus::{Bot, Project}; +use serde::Serialize; + +use crate::app::AppState; + +/// Largest file `read_file` returns whole. +const MAX_READ_BYTES: u64 = 16 * 1024 * 1024; +/// How deep and how wide an artifacts listing goes. +const MAX_DEPTH: usize = 4; +const MAX_ENTRIES: usize = 500; + +#[derive(Debug, Serialize)] +pub struct Artifact { + pub path: String, + /// Relative to the artifacts directory, `/`-separated. + pub rel: String, + pub name: String, + pub size: u64, + pub modified: Option, + pub mime: &'static str, + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, +} + +#[derive(Debug, Serialize)] +pub struct FileBody { + pub path: String, + pub name: String, + pub mime: &'static str, + pub size: u64, + #[serde(skip_serializing_if = "Option::is_none")] + pub text: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub base64: Option, + pub truncated: bool, +} + +/// Where a `read_file` path is resolved from and confined to. +pub enum Scope<'a> { + Project(&'a Project), + Bot(&'a Bot, &'a Project), +} + +pub fn artifacts_dir(app: &AppState, project: &Project) -> PathBuf { + crate::paths::artifacts_dir(&app.cfg, &project.dir_name) +} + +pub fn list_artifacts(app: &AppState, project: &Project) -> Vec { + let root = artifacts_dir(app, project); + let mut out = Vec::new(); + walk(&root, &root, 0, &mut out); + out.sort_by(|a, b| b.modified.cmp(&a.modified)); + out +} + +fn walk(root: &Path, dir: &Path, depth: usize, out: &mut Vec) { + let Ok(entries) = std::fs::read_dir(dir) else { + return; + }; + for entry in entries.flatten() { + if out.len() >= MAX_ENTRIES { + return; + } + let path = entry.path(); + let name = entry.file_name().to_string_lossy().to_string(); + if name.starts_with('.') { + continue; + } + let Ok(meta) = entry.metadata() else { + continue; + }; + if meta.is_dir() { + if depth < MAX_DEPTH { + walk(root, &path, depth + 1, out); + } + continue; + } + let rel = path + .strip_prefix(root) + .map(|r| r.to_string_lossy().replace('\\', "/")) + .unwrap_or_else(|_| name.clone()); + let mime = mime_for(&name); + out.push(Artifact { + path: path.display().to_string(), + rel, + title: (mime == "text/markdown").then(|| heading(&path)).flatten(), + name, + size: meta.len(), + modified: meta + .modified() + .ok() + .map(|t| chrono::DateTime::::from(t).to_rfc3339()), + mime, + }); + } +} + +/// A markdown file's first `# ` heading. +fn heading(path: &Path) -> Option { + use std::io::Read; + let mut head = vec![0u8; 4096]; + let n = std::fs::File::open(path).ok()?.read(&mut head).ok()?; + String::from_utf8_lossy(&head[..n]) + .lines() + .find_map(|l| l.strip_prefix("# ").map(|t| t.trim().to_string())) +} + +pub fn read_file(app: &AppState, scope: Scope<'_>, path: &str) -> anyhow::Result { + let (base, roots) = match scope { + Scope::Project(project) => { + let artifacts = artifacts_dir(app, project); + (artifacts.clone(), vec![artifacts]) + } + Scope::Bot(bot, project) => { + let workspace = PathBuf::from(&bot.workspace_path); + let bot_root = workspace + .parent() + .map(Path::to_path_buf) + .unwrap_or(workspace.clone()); + (workspace, vec![bot_root, artifacts_dir(app, project)]) + } + }; + let resolved = base + .join(path) + .canonicalize() + .map_err(|_| anyhow::anyhow!("file not found: {path}"))?; + let allowed = roots + .iter() + .filter_map(|root| root.canonicalize().ok()) + .any(|root| resolved.starts_with(root)); + anyhow::ensure!( + allowed, + "{path} is outside the bot's directory and the project artifacts" + ); + let meta = resolved.metadata()?; + anyhow::ensure!(meta.is_file(), "{path} is not a file"); + let name = resolved + .file_name() + .map(|n| n.to_string_lossy().to_string()) + .unwrap_or_default(); + let mime = mime_for(&name); + let truncated = meta.len() > MAX_READ_BYTES; + let bytes = read_capped(&resolved, MAX_READ_BYTES)?; + let (text, base64) = if mime.starts_with("image/") || mime == "application/pdf" { + ( + None, + Some(base64::engine::general_purpose::STANDARD.encode(&bytes)), + ) + } else { + match String::from_utf8(bytes) { + Ok(text) => (Some(text), None), + Err(e) => ( + None, + Some(base64::engine::general_purpose::STANDARD.encode(e.into_bytes())), + ), + } + }; + Ok(FileBody { + path: resolved.display().to_string(), + name, + mime, + size: meta.len(), + text, + base64, + truncated, + }) +} + +fn read_capped(path: &Path, max: u64) -> anyhow::Result> { + use std::io::Read; + let mut bytes = Vec::new(); + std::fs::File::open(path)? + .take(max) + .read_to_end(&mut bytes)?; + Ok(bytes) +} + +pub fn mime_for(name: &str) -> &'static str { + let ext = name.rsplit_once('.').map(|(_, e)| e.to_ascii_lowercase()); + match ext.as_deref() { + Some("md" | "markdown") => "text/markdown", + Some("png") => "image/png", + Some("jpg" | "jpeg") => "image/jpeg", + Some("gif") => "image/gif", + Some("webp") => "image/webp", + Some("svg") => "image/svg+xml", + Some("pdf") => "application/pdf", + Some("json") => "application/json", + Some("csv") => "text/csv", + Some("html" | "htm") => "text/html", + Some( + "rs" | "ts" | "tsx" | "js" | "jsx" | "mjs" | "py" | "swift" | "go" | "java" | "kt" + | "c" | "h" | "cpp" | "cs" | "rb" | "sh" | "ps1" | "toml" | "yaml" | "yml" | "css" + | "sql" | "xml", + ) => "text/x-code", + Some("txt" | "log") | None => "text/plain", + _ => "application/octet-stream", + } +} diff --git a/crates/gravityd/src/chat/model.rs b/crates/gravityd/src/chat/model.rs new file mode 100644 index 0000000..de47370 --- /dev/null +++ b/crates/gravityd/src/chat/model.rs @@ -0,0 +1,173 @@ +//! The chat wire model: a bot's conversation as turns. See +//! `docs/superpowers/specs/2026-09-16-chat-pane-design.md`. + +use chrono::{DateTime, Utc}; +use serde::Serialize; + +/// One turn: what woke the bot, and everything it did until it stopped. +#[derive(Debug, Clone, Serialize, PartialEq)] +pub struct ChatTurn { + /// The transcript id of the record that opened the turn. + pub id: String, + pub bot_id: String, + pub started_at: DateTime, + #[serde(skip_serializing_if = "Option::is_none")] + pub ended_at: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub duration_ms: Option, + pub open: bool, + pub trigger: Trigger, + pub items: Vec, + pub stats: Stats, +} + +#[derive(Debug, Clone, Serialize, PartialEq)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum Trigger { + /// The owner: from the app's composer, or typed into the terminal. + Owner { text: String, via: OwnerVia }, + /// Another bot's message on the bus. + Bus { + from: String, + msg_kind: String, + num: i64, + text: String, + #[serde(skip_serializing_if = "Option::is_none")] + task_id: Option, + }, + Routine { + name: String, + text: String, + #[serde(skip_serializing_if = "Option::is_none")] + run_id: Option, + }, + /// A decision ruling or notice delivered by the registry. + Ruling { decision_id: String, text: String }, + /// The session carried on after its context was compacted. + Resumed, + /// A background task finishing, a scheduled wake-up, anything else the + /// runtime started a turn for. + Background { text: String }, +} + +#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum OwnerVia { + Chat, + Terminal, +} + +#[derive(Debug, Clone, Serialize, PartialEq)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum ChatItem { + Text { + id: String, + markdown: String, + }, + Step(Step), + /// `send_message` on the bus. + Sent { + id: String, + to: String, + msg_kind: String, + body: String, + }, + /// `complete_task`: the result and the files it handed back. + Completed { + id: String, + task_id: String, + result: String, + artifacts: Vec, + }, + /// `raise_decision`: the bot asked the owner to decide. + Decision { + id: String, + #[serde(skip_serializing_if = "Option::is_none")] + decision_id: Option, + title: String, + }, + Aside { + id: String, + kind: AsideKind, + text: String, + }, +} + +#[derive(Debug, Clone, Serialize, PartialEq)] +pub struct Step { + /// The tool call's id. + pub id: String, + pub tool: String, + pub title: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub subtitle: Option, + pub status: StepStatus, + /// Housekeeping (inbox checks, tool lookups) that folds away. + pub minor: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub added: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub removed: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub images: Vec, +} + +#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum StepStatus { + Running, + Ok, + Error, +} + +#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum AsideKind { + /// The context was compacted. + Compacted, + /// The owner interrupted the turn. + Interrupted, + /// A message arrived while the bot was working. + Incoming, +} + +/// An image a step produced or looked at, fetched with `get_chat_image`. +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +pub struct ImageRef { + pub id: String, + pub mime: String, +} + +/// A file a result names, opened with `read_file`. +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +pub struct FileRef { + pub path: String, + pub name: String, +} + +#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq)] +pub struct Stats { + pub commands: u32, + pub reads: u32, + pub edits: u32, + pub added: u32, + pub removed: u32, + pub sent: u32, + pub images: u32, + pub errors: u32, +} + +/// The heavy half of a step, loaded when the owner opens it. +#[derive(Debug, Clone, Default, Serialize, PartialEq)] +pub struct StepDetail { + #[serde(skip_serializing_if = "Option::is_none")] + pub input: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub command: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub output: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub diff: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub content: Option, +} diff --git a/crates/gravityd/src/chat/steps.rs b/crates/gravityd/src/chat/steps.rs new file mode 100644 index 0000000..a8672c3 --- /dev/null +++ b/crates/gravityd/src/chat/steps.rs @@ -0,0 +1,269 @@ +//! How a tool call reads in the chat: a human title for the step, or, for +//! Gravity's own bus tools, the message, result or decision it really was. + +use serde_json::Value; + +use super::model::FileRef; + +/// The MCP server name Gravity registers its bus under. +const BUS_PREFIX: &str = "mcp__gravity-bus__"; + +/// Bus tools that are housekeeping, not something the owner wants to read. +const MINOR_BUS_TOOLS: &[&str] = &[ + "check_inbox", + "list_bots", + "get_self", + "list_routines", + "list_decisions", + "get_decision", + "list_tags", +]; + +/// Built-in tools that are plumbing rather than work. +const MINOR_TOOLS: &[&str] = &[ + "ToolSearch", + "Skill", + "TaskStop", + "TaskOutput", + "TodoWrite", + "ScheduleWakeup", + "Monitor", +]; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ToolKind { + Command, + Read, + Edit, + Other, +} + +#[derive(Debug, PartialEq)] +pub struct Described { + pub title: String, + pub subtitle: Option, + pub minor: bool, + pub kind: ToolKind, +} + +/// A bus tool call, read as what it did on the bus. +#[derive(Debug, PartialEq)] +pub enum BusCall { + Sent { + to: String, + kind: String, + body: String, + }, + Completed { + task_id: String, + result: String, + artifacts: Vec, + }, + Decision { + title: String, + }, +} + +pub fn bus_call(name: &str, input: &Value) -> Option { + let tool = name.strip_prefix(BUS_PREFIX)?; + let text = |key: &str| { + input + .get(key) + .and_then(Value::as_str) + .unwrap_or_default() + .to_string() + }; + match tool { + "send_message" => Some(BusCall::Sent { + to: text("to"), + kind: input + .get("kind") + .and_then(Value::as_str) + .unwrap_or("note") + .to_string(), + body: text("body"), + }), + "complete_task" => Some(BusCall::Completed { + task_id: text("task_id"), + result: text("result"), + artifacts: input + .get("artifacts") + .and_then(Value::as_array) + .into_iter() + .flatten() + .filter_map(Value::as_str) + .map(file_ref) + .collect(), + }), + "raise_decision" => Some(BusCall::Decision { + title: text("title"), + }), + _ => None, + } +} + +pub fn file_ref(path: &str) -> FileRef { + FileRef { + path: path.to_string(), + name: base_name(path).to_string(), + } +} + +pub fn describe(name: &str, input: &Value) -> Described { + let field = |key: &str| input.get(key).and_then(Value::as_str); + let path = field("file_path").or_else(|| field("notebook_path")); + let file = path.map(base_name).unwrap_or("a file"); + let (title, subtitle, kind) = match name { + "Bash" => ( + field("description") + .map(str::to_string) + .unwrap_or_else(|| "Ran a command".to_string()), + field("command").map(first_line), + ToolKind::Command, + ), + "Read" => ( + format!("Read {file}"), + path.map(str::to_string), + ToolKind::Read, + ), + "Edit" | "MultiEdit" | "NotebookEdit" => ( + format!("Edited {file}"), + path.map(str::to_string), + ToolKind::Edit, + ), + "Write" => ( + format!("Wrote {file}"), + path.map(str::to_string), + ToolKind::Edit, + ), + "Grep" | "Glob" => ( + format!("Searched for {}", field("pattern").unwrap_or("files")), + field("path").map(str::to_string), + ToolKind::Read, + ), + "WebFetch" => ( + format!("Fetched {}", field("url").map(host).unwrap_or("a page")), + field("url").map(str::to_string), + ToolKind::Other, + ), + "WebSearch" => ( + format!( + "Searched the web for {}", + field("query").unwrap_or("something") + ), + None, + ToolKind::Other, + ), + "Task" | "Agent" => ( + format!( + "Ran a sub-agent: {}", + field("description").unwrap_or("a task") + ), + None, + ToolKind::Other, + ), + "TodoWrite" => ("Updated the plan".to_string(), None, ToolKind::Other), + other => match other.strip_prefix("mcp__") { + Some(rest) => { + let (server, tool) = rest.split_once("__").unwrap_or(("mcp", rest)); + ( + tool.replace('_', " "), + Some(server.to_string()), + ToolKind::Other, + ) + } + None => (other.to_string(), None, ToolKind::Other), + }, + }; + let minor = MINOR_TOOLS.contains(&name) + || name + .strip_prefix(BUS_PREFIX) + .is_some_and(|tool| MINOR_BUS_TOOLS.contains(&tool)); + Described { + title, + subtitle: subtitle.map(|s| truncate(&s, 160)), + minor, + kind, + } +} + +/// The last component of a path written on either platform. +pub fn base_name(path: &str) -> &str { + path.rsplit(['/', '\\']) + .find(|part| !part.is_empty()) + .unwrap_or(path) +} + +fn host(url: &str) -> &str { + let rest = url.split_once("://").map_or(url, |(_, rest)| rest); + rest.split('/').next().unwrap_or(rest) +} + +fn first_line(text: &str) -> String { + text.lines().next().unwrap_or_default().to_string() +} + +/// At most `max` characters, with an ellipsis when cut. +pub fn truncate(text: &str, max: usize) -> String { + if text.chars().count() <= max { + return text.to_string(); + } + let mut out: String = text.chars().take(max).collect(); + out.push('…'); + out +} + +/// The last `max` characters, for output whose end is what matters. +pub fn tail(text: &str, max: usize) -> String { + let count = text.chars().count(); + if count <= max { + return text.to_string(); + } + let mut out = String::from("…"); + out.extend(text.chars().skip(count - max)); + out +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn titles_common_tools() { + let bash = describe( + "Bash", + &json!({"command": "cargo test\nmore", "description": "Run tests"}), + ); + assert_eq!(bash.title, "Run tests"); + assert_eq!(bash.subtitle.as_deref(), Some("cargo test")); + assert_eq!(bash.kind, ToolKind::Command); + let edit = describe("Edit", &json!({"file_path": "C:\\work\\src\\app.ts"})); + assert_eq!(edit.title, "Edited app.ts"); + assert!(describe("mcp__gravity-bus__check_inbox", &json!({})).minor); + assert_eq!( + describe("mcp__chrome__take_screenshot", &json!({})).title, + "take screenshot" + ); + } + + #[test] + fn reads_bus_tools_as_what_they_did() { + let call = bus_call( + "mcp__gravity-bus__complete_task", + &json!({"task_id": "t", "result": "done", "artifacts": ["/a/b/report.md"]}), + ); + assert_eq!( + call, + Some(BusCall::Completed { + task_id: "t".to_string(), + result: "done".to_string(), + artifacts: vec![FileRef { + path: "/a/b/report.md".to_string(), + name: "report.md".to_string() + }], + }) + ); + assert_eq!(bus_call("Bash", &json!({})), None); + } +} diff --git a/crates/gravityd/src/chat/store.rs b/crates/gravityd/src/chat/store.rs new file mode 100644 index 0000000..fac0835 --- /dev/null +++ b/crates/gravityd/src/chat/store.rs @@ -0,0 +1,288 @@ +//! Per-bot chat state kept in step with the transcript. A bot's chat is +//! built the first time a client asks for it, then extended line by line as +//! the transcript grows; a new transcript file (a fresh session) rebuilds it. + +use std::collections::HashMap; +use std::fs::File; +use std::io::{BufRead, BufReader, Read, Seek, SeekFrom}; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; + +use bus::{Bot, BotRuntime, BotState}; +use serde_json::Value; + +use crate::app::AppState; + +use super::builder::{result_text, Builder}; +use super::model::{ChatTurn, StepDetail}; +use super::steps; + +const MAX_INPUT_CHARS: usize = 4_000; +const MAX_OUTPUT_CHARS: usize = 12_000; +const MAX_CONTENT_CHARS: usize = 8_000; +const MAX_DIFF_LINES: usize = 400; + +struct BotChat { + path: Option, + /// Bytes of the transcript already folded in; always at a line boundary. + offset: u64, + builder: Builder, +} + +#[derive(Default)] +pub struct ChatStore { + bots: Mutex>>>, +} + +impl ChatStore { + /// Brings the bot's chat up to date and returns the turns that changed. + pub fn refresh(&self, app: &AppState, bot: &Bot) -> anyhow::Result> { + let (chat, fresh) = self.entry(app, bot)?; + let mut chat = lock(&chat); + let path = transcript(app, bot); + if path != chat.path { + *chat = BotChat { + path: path.clone(), + offset: 0, + builder: Builder::new(&bot.id, names(app, bot)), + }; + } + if let Some(path) = &path { + let offset = chat.offset; + chat.offset = read_from(path, offset, &mut chat.builder)?; + } + let changed = chat.builder.take_changed(); + // The first read is a snapshot, not news. + let changed = if fresh { Vec::new() } else { changed }; + Ok(settle(changed, &chat.builder.turns, busy(app, bot))) + } + + /// Up to `limit` turns before `before` (or the newest), oldest first, + /// and whether older ones exist. + pub fn page( + &self, + app: &AppState, + bot: &Bot, + before: Option<&str>, + limit: usize, + ) -> anyhow::Result<(Vec, bool)> { + self.refresh(app, bot)?; + let (chat, _) = self.entry(app, bot)?; + let chat = lock(&chat); + let turns = &chat.builder.turns; + let end = match before { + Some(id) => turns.iter().position(|t| t.id == id).unwrap_or(turns.len()), + None => turns.len(), + }; + let start = end.saturating_sub(limit); + let page = settle(turns[start..end].to_vec(), turns, busy(app, bot)); + Ok((page, start > 0)) + } + + pub fn step(&self, app: &AppState, bot: &Bot, item_id: &str) -> anyhow::Result { + let (chat, _) = self.entry(app, bot)?; + let chat = lock(&chat); + let path = chat + .path + .clone() + .ok_or_else(|| anyhow::anyhow!("no transcript"))?; + let locator = chat + .builder + .steps + .get(item_id) + .cloned() + .ok_or_else(|| anyhow::anyhow!("no step {item_id}"))?; + drop(chat); + let use_line: Value = serde_json::from_str(&line_at(&path, locator.use_offset)?)?; + let call = use_line["message"]["content"] + .as_array() + .into_iter() + .flatten() + .find(|b| b["id"] == item_id) + .cloned() + .unwrap_or(Value::Null); + let input = &call["input"]; + let mut detail = StepDetail { + input: Some(steps::truncate( + &serde_json::to_string_pretty(input).unwrap_or_default(), + MAX_INPUT_CHARS, + )), + command: input["command"].as_str().map(str::to_string), + content: input["content"] + .as_str() + .map(|c| steps::truncate(c, MAX_CONTENT_CHARS)), + ..StepDetail::default() + }; + if let Some(offset) = locator.result_offset { + let record: Value = serde_json::from_str(&line_at(&path, offset)?)?; + let block = record["message"]["content"] + .as_array() + .into_iter() + .flatten() + .find(|b| b["tool_use_id"] == item_id); + detail.output = block + .and_then(result_text) + .map(|text| steps::tail(&text, MAX_OUTPUT_CHARS)); + detail.diff = diff_lines(&record["toolUseResult"]); + } + Ok(detail) + } + + /// An image's media type and base64 bytes. + pub fn image( + &self, + app: &AppState, + bot: &Bot, + image_id: &str, + ) -> anyhow::Result<(String, String)> { + let (chat, _) = self.entry(app, bot)?; + let chat = lock(&chat); + let path = chat + .path + .clone() + .ok_or_else(|| anyhow::anyhow!("no transcript"))?; + let at = *chat + .builder + .images + .get(image_id) + .ok_or_else(|| anyhow::anyhow!("no image {image_id}"))?; + drop(chat); + let record: Value = serde_json::from_str(&line_at(&path, at.offset)?)?; + let source = &record["message"]["content"][at.block]["content"][at.inner]["source"]; + let data = source["data"] + .as_str() + .ok_or_else(|| anyhow::anyhow!("image bytes missing"))?; + let mime = source["media_type"].as_str().unwrap_or("image/png"); + Ok((mime.to_string(), data.to_string())) + } + + /// Bots whose chat has been loaded, for the watcher to keep current. + pub fn loaded(&self) -> Vec { + self.bots + .lock() + .unwrap_or_else(|e| e.into_inner()) + .keys() + .cloned() + .collect() + } + + /// The bot's chat, and whether it was created just now. + fn entry(&self, app: &AppState, bot: &Bot) -> anyhow::Result<(Arc>, bool)> { + let mut bots = self.bots.lock().unwrap_or_else(|e| e.into_inner()); + if let Some(chat) = bots.get(&bot.id) { + return Ok((chat.clone(), false)); + } + let chat = Arc::new(Mutex::new(BotChat { + path: None, + offset: 0, + builder: Builder::new(&bot.id, names(app, bot)), + })); + bots.insert(bot.id.clone(), chat.clone()); + Ok((chat, true)) + } +} + +fn lock(chat: &Mutex) -> std::sync::MutexGuard<'_, BotChat> { + chat.lock().unwrap_or_else(|e| e.into_inner()) +} + +/// The file a bot's runtime records its conversation in, if it exists yet. +pub fn transcript(app: &AppState, bot: &Bot) -> Option { + if bot.is_linked() { + return None; + } + let workspace = Path::new(&bot.workspace_path); + match bot.runtime { + BotRuntime::CodexCli => { + let path = crate::runtime::codex::transcript_path(workspace); + path.exists().then_some(path) + } + BotRuntime::ClaudeCode => crate::activity::newest_transcript( + &crate::activity::transcript_dir(&app.cfg.user_home, workspace), + ), + } +} + +/// Upper-case envelope names to real bot names, for the bot's project. +fn names(app: &AppState, bot: &Bot) -> HashMap { + app.db + .list_bots(Some(&bot.project_id)) + .unwrap_or_default() + .into_iter() + .map(|b| (b.name.to_uppercase(), b.name)) + .collect() +} + +/// Whether the bot could still be inside a turn. A turn whose bot is idle, +/// stopped or crashed is shown as finished even if its end was never written. +fn busy(app: &AppState, bot: &Bot) -> bool { + matches!( + app.supervisor.state(&bot.id).0, + BotState::Working | BotState::WaitingForApproval | BotState::Starting + ) +} + +fn settle(mut page: Vec, all: &[ChatTurn], busy: bool) -> Vec { + let last = all.last().map(|t| t.id.as_str()); + for turn in &mut page { + if turn.open && (!busy || Some(turn.id.as_str()) != last) { + turn.open = false; + } + } + page +} + +/// Folds the complete lines after `offset` into the builder and returns the +/// offset of the first line not yet complete. +fn read_from(path: &Path, offset: u64, builder: &mut Builder) -> anyhow::Result { + let mut file = File::open(path)?; + let len = file.metadata()?.len(); + // A shorter file than we have read is a rewritten one: start over. + let offset = if len < offset { 0 } else { offset }; + file.seek(SeekFrom::Start(offset))?; + let mut bytes = Vec::new(); + file.read_to_end(&mut bytes)?; + let mut start = 0usize; + while let Some(end) = bytes[start..].iter().position(|&b| b == b'\n') { + let line = &bytes[start..start + end]; + if let Ok(text) = std::str::from_utf8(line) { + builder.push_line(offset + start as u64, text); + } + start += end + 1; + } + Ok(offset + start as u64) +} + +fn line_at(path: &Path, offset: u64) -> anyhow::Result { + let mut file = File::open(path)?; + file.seek(SeekFrom::Start(offset))?; + let mut line = String::new(); + BufReader::new(file).read_line(&mut line)?; + Ok(line) +} + +/// A unified diff from Claude Code's structured patch, hunk headers included. +fn diff_lines(result: &Value) -> Option> { + let hunks = result["structuredPatch"] + .as_array() + .filter(|h| !h.is_empty())?; + let mut lines = Vec::new(); + for hunk in hunks { + let n = |key: &str| hunk[key].as_i64().unwrap_or(0); + lines.push(format!( + "@@ -{},{} +{},{} @@", + n("oldStart"), + n("oldLines"), + n("newStart"), + n("newLines") + )); + for line in hunk["lines"].as_array().into_iter().flatten() { + lines.push(line.as_str().unwrap_or_default().to_string()); + if lines.len() >= MAX_DIFF_LINES { + lines.push("… diff truncated".to_string()); + return Some(lines); + } + } + } + Some(lines) +} diff --git a/crates/gravityd/src/events.rs b/crates/gravityd/src/events.rs index 65d8775..9cd260a 100644 --- a/crates/gravityd/src/events.rs +++ b/crates/gravityd/src/events.rs @@ -36,6 +36,11 @@ pub enum Push { DeliveryUpdate { delivery: Delivery, }, + /// Turns of a bot's chat that are new or changed, oldest first. + ChatTurns { + bot_id: String, + turns: Vec, + }, RoutineRunUpdate { routine_run: RoutineRun, }, diff --git a/crates/gravityd/src/lib.rs b/crates/gravityd/src/lib.rs index d6b2268..6de0471 100644 --- a/crates/gravityd/src/lib.rs +++ b/crates/gravityd/src/lib.rs @@ -7,6 +7,7 @@ pub mod app; pub mod backup; pub mod botmgmt; pub mod channel; +pub mod chat; pub mod config; pub mod db; pub mod decisions; diff --git a/crates/gravityd/src/paths.rs b/crates/gravityd/src/paths.rs index fcec437..4a37d4d 100644 --- a/crates/gravityd/src/paths.rs +++ b/crates/gravityd/src/paths.rs @@ -279,22 +279,26 @@ fn hook_settings(daemon_port: u16, bot_token_env: &str) -> serde_json::Value { }] }]) }; - // Notification carries Claude Code's stdin payload through untouched, so - // the daemon can read its `message` and tell a permission prompt apart - // from the "waiting for your input" idle ping. The event name rides in the - // query string because the body is no longer ours to shape. - let notification = serde_json::json!([{ - "hooks": [{ - "type": "command", - "command": format!( - "curl -fsS -m 3 -X POST \ - 'http://127.0.0.1:{daemon_port}/hook?event=Notification' \ - -H \"Authorization: Bearer ${{{bot_token_env}}}\" \ - -H 'Content-Type: application/json' --data-binary @- \ - >/dev/null 2>&1 || true" - ) - }] - }]); + // Notification and Stop carry Claude Code's stdin payload through + // untouched: Notification so the daemon can read its `message` and tell a + // permission prompt apart from the "waiting for your input" idle ping, + // Stop so it learns the `transcript_path` a finished routine run is read + // from. The event name rides in the query string because the body is no + // longer ours to shape. + let forwarding = |event: &str| { + serde_json::json!([{ + "hooks": [{ + "type": "command", + "command": format!( + "curl -fsS -m 3 -X POST \ + 'http://127.0.0.1:{daemon_port}/hook?event={event}' \ + -H \"Authorization: Bearer ${{{bot_token_env}}}\" \ + -H 'Content-Type: application/json' --data-binary @- \ + >/dev/null 2>&1 || true" + ) + }] + }]) + }; // SessionStart additionally reports the session's inbox socket (and its // messaging token) so the daemon can deliver bus messages through it // instead of the terminal. @@ -333,8 +337,8 @@ fn hook_settings(daemon_port: u16, bot_token_env: &str) -> serde_json::Value { // executing again: nothing else reports that a pending permission // prompt was answered. "PostToolUse": hook_cmd("PostToolUse"), - "Stop": hook_cmd("Stop"), - "Notification": notification, + "Stop": forwarding("Stop"), + "Notification": forwarding("Notification"), "SessionEnd": hook_cmd("SessionEnd") } }) diff --git a/crates/gravityd/src/scheduler/dispatch.rs b/crates/gravityd/src/scheduler/dispatch.rs index 7e047ba..39da676 100644 --- a/crates/gravityd/src/scheduler/dispatch.rs +++ b/crates/gravityd/src/scheduler/dispatch.rs @@ -227,6 +227,8 @@ fn routine_run_id_from_transcript(path: &Path) -> Option { .join("\n"), _ => return None, }; + // Claude Code wraps a delivered message in its own peer preamble. + let text = crate::chat::unwrap_peer(&text); if !text.starts_with("[routine \"") { return None; } @@ -273,6 +275,25 @@ mod tests { assert_eq!(routine_run_id_from_transcript(&path), Some(run_id)); } + #[test] + fn finds_the_run_id_inside_claude_codes_peer_wrapper() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("session.jsonl"); + let run_id = bus::new_id(); + let record = json!({ + "type": "user", + "isMeta": true, + "origin": {"kind": "peer"}, + "message": {"content": format!( + "Another Claude session sent a message:\n[routine \"nightly\" #7 · run_id \ + {run_id}] run it\n\nThis came from another Claude session." + )} + }); + fs::write(&path, format!("{record}\n")).unwrap(); + + assert_eq!(routine_run_id_from_transcript(&path), Some(run_id)); + } + #[test] fn ordinary_user_turn_has_no_run_id() { let dir = tempfile::tempdir().unwrap(); diff --git a/crates/gravityd/src/server.rs b/crates/gravityd/src/server.rs index a5d5ebe..24cd7b9 100644 --- a/crates/gravityd/src/server.rs +++ b/crates/gravityd/src/server.rs @@ -92,6 +92,7 @@ pub fn spawn_workers(app: &Arc) { tokio::spawn(scheduler.run()); tokio::spawn(crate::activity::watch(app.clone())); + tokio::spawn(crate::chat::watch(app.clone())); tokio::spawn(crate::decisions::run_watch(app.clone())); if app.cfg.retention.enabled { diff --git a/crates/gravityd/src/ws.rs b/crates/gravityd/src/ws.rs index 7187eb9..6de8567 100644 --- a/crates/gravityd/src/ws.rs +++ b/crates/gravityd/src/ws.rs @@ -18,6 +18,7 @@ use tokio::task::JoinHandle; use crate::app::{AppState, DAEMON_VERSION, PROTOCOL_VERSION}; mod admin; +mod chat; mod decisions; mod decisions_publish; mod dispatch; diff --git a/crates/gravityd/src/ws/chat.rs b/crates/gravityd/src/ws/chat.rs new file mode 100644 index 0000000..63c23b6 --- /dev/null +++ b/crates/gravityd/src/ws/chat.rs @@ -0,0 +1,138 @@ +//! Chat requests: a bot's turns, a step's detail, images and files. They read +//! transcripts and files, so each runs off the connection's task. + +use std::sync::Arc; + +use serde_json::{json, Value}; + +use crate::app::AppState; +use crate::chat::files::{self, Scope}; + +use super::Conn; + +/// Turns sent when the client does not ask for a number. +const DEFAULT_PAGE: usize = 30; +const MAX_PAGE: usize = 200; + +impl Conn { + pub(super) fn list_chat(&self, req_id: &Value, req: &Value) -> anyhow::Result<()> { + let bot_id = Self::str_field(req, "bot_id")?.to_string(); + let before = req + .get("before") + .and_then(Value::as_str) + .map(str::to_string); + let limit = req + .get("limit") + .and_then(Value::as_u64) + .map_or(DEFAULT_PAGE, |n| (n as usize).clamp(1, MAX_PAGE)); + self.blocking(req_id, move |app| { + let bot = live_bot(app, &bot_id)?; + let (turns, has_more) = app.chat.page(app, &bot, before.as_deref(), limit)?; + Ok(json!({ "type": "chat", "bot_id": bot_id, "turns": turns, "has_more": has_more })) + }); + Ok(()) + } + + pub(super) fn get_chat_step(&self, req_id: &Value, req: &Value) -> anyhow::Result<()> { + let bot_id = Self::str_field(req, "bot_id")?.to_string(); + let item_id = Self::str_field(req, "item_id")?.to_string(); + self.blocking(req_id, move |app| { + let bot = live_bot(app, &bot_id)?; + let detail = app.chat.step(app, &bot, &item_id)?; + Ok(json!({ + "type": "chat_step", "bot_id": bot_id, "item_id": item_id, "detail": detail + })) + }); + Ok(()) + } + + pub(super) fn get_chat_image(&self, req_id: &Value, req: &Value) -> anyhow::Result<()> { + let bot_id = Self::str_field(req, "bot_id")?.to_string(); + let image_id = Self::str_field(req, "image_id")?.to_string(); + self.blocking(req_id, move |app| { + let bot = live_bot(app, &bot_id)?; + let (mime, base64) = app.chat.image(app, &bot, &image_id)?; + Ok(json!({ + "type": "file", + "file": { "name": image_id, "mime": mime, "base64": base64, "truncated": false } + })) + }); + Ok(()) + } + + pub(super) fn list_artifacts(&self, req_id: &Value, req: &Value) -> anyhow::Result<()> { + let project_id = Self::str_field(req, "project_id")?.to_string(); + self.blocking(req_id, move |app| { + let project = live_project(app, &project_id)?; + let artifacts = files::list_artifacts(app, &project); + Ok(json!({ "type": "artifacts", "project_id": project_id, "artifacts": artifacts })) + }); + Ok(()) + } + + /// A file from the project's artifacts (`project_id`), or from a bot's + /// directory and its project's artifacts (`bot_id`). + pub(super) fn read_file(&self, req_id: &Value, req: &Value) -> anyhow::Result<()> { + let path = Self::str_field(req, "path")?.to_string(); + let bot_id = req + .get("bot_id") + .and_then(Value::as_str) + .map(str::to_string); + let project_id = req + .get("project_id") + .and_then(Value::as_str) + .map(str::to_string); + self.blocking(req_id, move |app| { + let file = match (bot_id, project_id) { + (Some(bot_id), _) => { + let bot = live_bot(app, &bot_id)?; + let project = live_project(app, &bot.project_id)?; + files::read_file(app, Scope::Bot(&bot, &project), &path)? + } + (None, Some(project_id)) => { + let project = live_project(app, &project_id)?; + files::read_file(app, Scope::Project(&project), &path)? + } + (None, None) => anyhow::bail!("'bot_id' or 'project_id' is required"), + }; + Ok(json!({ "type": "file", "file": file })) + }); + Ok(()) + } + + /// Runs `work` on a blocking thread and replies with its result, or with + /// a `not_found` error the client can show. + fn blocking( + &self, + req_id: &Value, + work: impl FnOnce(&Arc) -> anyhow::Result + Send + 'static, + ) { + let (app, out, req_id) = (self.app.clone(), self.out.clone(), req_id.clone()); + tokio::task::spawn_blocking(move || { + let reply = match work(&app) { + Ok(mut reply) => { + reply["req_id"] = req_id; + reply + } + Err(e) => json!({ + "type": "error", "req_id": req_id, "code": "not_found", + "message": format!("{e:#}") + }), + }; + let _ = out.send(reply); + }); + } +} + +fn live_bot(app: &AppState, bot_id: &str) -> anyhow::Result { + app.db + .get_live_bot(bot_id)? + .ok_or_else(|| anyhow::anyhow!("bot not found")) +} + +fn live_project(app: &AppState, project_id: &str) -> anyhow::Result { + app.db + .get_project(project_id)? + .filter(|p| p.deleted_at.is_none()) + .ok_or_else(|| anyhow::anyhow!("project not found")) +} diff --git a/crates/gravityd/src/ws/dispatch.rs b/crates/gravityd/src/ws/dispatch.rs index 0a70511..f7a8d34 100644 --- a/crates/gravityd/src/ws/dispatch.rs +++ b/crates/gravityd/src/ws/dispatch.rs @@ -32,6 +32,11 @@ const READ_ONLY: &[&str] = &[ "count_pending_decisions", "list_peers", "list_peer_bots", + "list_chat", + "get_chat_step", + "get_chat_image", + "list_artifacts", + "read_file", ]; /// Requests that exercise the owner's ruling authority. @@ -138,6 +143,11 @@ impl Conn { "revoke_peer" => self.revoke_peer(&req_id, req), "list_peer_bots" => self.list_peer_bots(&req_id, req), "link_peer_bot" => self.link_peer_bot(&req_id, req), + "list_chat" => self.list_chat(&req_id, req), + "get_chat_step" => self.get_chat_step(&req_id, req), + "get_chat_image" => self.get_chat_image(&req_id, req), + "list_artifacts" => self.list_artifacts(&req_id, req), + "read_file" => self.read_file(&req_id, req), other => { self.reply_err( &req_id, diff --git a/crates/gravityd/tests/chat.rs b/crates/gravityd/tests/chat.rs new file mode 100644 index 0000000..80bd6f7 --- /dev/null +++ b/crates/gravityd/tests/chat.rs @@ -0,0 +1,184 @@ +//! The chat pane's daemon side: a bot's transcript served as turns over the +//! control plane, followed live as it grows, with step detail, images and +//! files on demand. + +mod common; + +use std::io::Write; +use std::path::{Path, PathBuf}; + +use common::*; +use serde_json::{json, Value}; + +struct Setup { + d: TestDaemon, + c: WsClient, + bot_id: String, + project_id: String, + transcript: PathBuf, +} + +/// A daemon whose Claude Code transcripts live under its own temp home, and +/// one bot with a transcript file to write into. +async fn setup() -> Setup { + let d = spawn_daemon_with(|cfg| cfg.user_home = cfg.home.join("user")).await; + let mut c = WsClient::connect(&d).await; + let created = c + .request(json!({"type": "create_project", "name": "app"})) + .await; + let project_id = created["project"]["id"].as_str().expect("pid").to_string(); + let bot = create_bot(&mut c, &project_id, "dev").await; + let bot_id = bot["id"].as_str().expect("id").to_string(); + let workspace = bot["workspace_path"] + .as_str() + .expect("workspace") + .to_string(); + let mangled: String = workspace + .chars() + .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '-' }) + .collect(); + let dir = d.app.cfg.user_home.join(".claude/projects").join(mangled); + std::fs::create_dir_all(&dir).expect("transcript dir"); + Setup { + transcript: dir.join("session.jsonl"), + d, + c, + bot_id, + project_id, + } +} + +fn append(path: &Path, records: &[Value]) { + let mut file = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(path) + .expect("open transcript"); + for record in records { + writeln!(file, "{record}").expect("write"); + } +} + +fn owner_chat(text: &str) -> Value { + json!({"type": "user", "uuid": "turn-1", "timestamp": "2026-10-01T10:00:00Z", + "isMeta": true, "origin": {"kind": "peer"}, + "message": {"content": format!( + "Another Claude session sent a message:\n[msg #7 from USER · chat] {text}")}}) +} + +#[tokio::test] +async fn a_transcript_is_served_as_turns_and_followed_live() { + let mut s = setup().await; + append( + &s.transcript, + &[ + owner_chat("screenshot the login page"), + json!({"type": "assistant", "uuid": "a1", "timestamp": "2026-10-01T10:00:02Z", + "message": {"content": [ + {"type": "text", "text": "Taking it now."}, + {"type": "tool_use", "id": "shot", "name": "mcp__chrome__screenshot", + "input": {"url": "http://localhost"}}]}}), + json!({"type": "user", "uuid": "r1", "timestamp": "2026-10-01T10:00:03Z", + "message": {"content": [{"type": "tool_result", "tool_use_id": "shot", + "content": [{"type": "image", "source": {"type": "base64", + "media_type": "image/png", "data": "iVBORw0KGgo="}}]}]}}), + ], + ); + + let chat = + s.c.request(json!({"type": "list_chat", "bot_id": s.bot_id})) + .await; + assert_eq!(chat["type"], "chat", "{chat}"); + let turn = &chat["turns"][0]; + assert_eq!(turn["trigger"]["kind"], "owner"); + assert_eq!(turn["trigger"]["via"], "chat"); + assert_eq!(turn["items"][0]["markdown"], "Taking it now."); + let image = &turn["items"][1]["images"][0]; + assert_eq!(image["mime"], "image/png"); + + let file = + s.c.request(json!({"type": "get_chat_image", "bot_id": s.bot_id, "image_id": image["id"]})) + .await; + assert_eq!(file["file"]["base64"], "iVBORw0KGgo="); + + let step = + s.c.request(json!({"type": "get_chat_step", "bot_id": s.bot_id, "item_id": "shot"})) + .await; + assert!(step["detail"]["input"] + .as_str() + .expect("input") + .contains("localhost")); + + // The bot keeps going: the new text arrives as a push for the same turn. + append( + &s.transcript, + &[ + json!({"type": "assistant", "uuid": "a2", "timestamp": "2026-10-01T10:00:04Z", + "message": {"content": [{"type": "text", "text": "Here it is."}]}}), + ], + ); + let pushed = + s.c.wait_for(|v| v["type"] == "chat_turns" && v["bot_id"] == json!(s.bot_id.clone())) + .await; + assert_eq!(pushed["turns"][0]["id"], "turn-1"); + let items = pushed["turns"][0]["items"].as_array().expect("items"); + assert_eq!(items.last().expect("item")["markdown"], "Here it is."); +} + +#[tokio::test] +async fn files_are_confined_to_the_bot_and_the_project_artifacts() { + let mut s = setup().await; + let project = + s.d.app + .db + .get_project(&s.project_id) + .expect("db") + .expect("project"); + let artifacts = gravityd::paths::artifacts_dir(&s.d.app.cfg, &project.dir_name); + std::fs::write(artifacts.join("report.md"), "# Weekly report\n\nAll green.").expect("write"); + + let listed = + s.c.request(json!({"type": "list_artifacts", "project_id": s.project_id})) + .await; + let entry = &listed["artifacts"][0]; + assert_eq!(entry["name"], "report.md"); + assert_eq!(entry["title"], "Weekly report"); + assert_eq!(entry["mime"], "text/markdown"); + + let read = + s.c.request(json!({"type": "read_file", "project_id": s.project_id, "path": "report.md"})) + .await; + assert_eq!(read["file"]["text"], "# Weekly report\n\nAll green."); + + // The bot scope reaches its own workspace and the shared artifacts... + let read = + s.c.request(json!({"type": "read_file", "bot_id": s.bot_id, "path": "CLAUDE.md"})) + .await; + assert_eq!(read["type"], "file", "{read}"); + let read = + s.c.request(json!({ + "type": "read_file", "bot_id": s.bot_id, + "path": artifacts.join("report.md").display().to_string() + })) + .await; + assert_eq!(read["type"], "file", "{read}"); + + // ...and nothing else, however the path is written. + for escape in ["../../../../secrets/client.token", "/etc/hosts"] { + let refused = + s.c.request(json!({"type": "read_file", "bot_id": s.bot_id, "path": escape})) + .await; + assert_eq!( + refused["type"], "error", + "{escape} must be refused: {refused}" + ); + } +} + +#[tokio::test] +async fn the_chat_capability_is_advertised() { + let d = spawn_daemon().await; + let hello = raw_hello(&d, d.app.secrets.client_token()).await; + let caps = hello["capabilities"].as_array().expect("capabilities"); + assert!(caps.contains(&json!("chat"))); +} diff --git a/docs/protocol.md b/docs/protocol.md index dc377ac..54d7a70 100644 --- a/docs/protocol.md +++ b/docs/protocol.md @@ -89,6 +89,11 @@ Codes: `auth_failed`, `unsupported_version`, `not_found`, `invalid_request`, | `revoke_peer` | `peer_id` | `peer` | | `list_peer_bots` | `peer_id` | `peer_bots` (`bots`: id, name, description, avatar, runtime, project) | | `link_peer_bot` | `peer_id, remote_bot_id, project_id` | `bot` (a linked bot; `peer` is set on it) | +| `list_chat` | `bot_id, before?` (a turn id), `limit?` (default 30, max 200) | `chat` (`turns` oldest first, `has_more`) | +| `get_chat_step` | `bot_id, item_id` | `chat_step` (`detail`: `input?, command?, output?, diff?, content?`) | +| `get_chat_image` | `bot_id, image_id` | `file` | +| `list_artifacts` | `project_id` | `artifacts` (newest first) | +| `read_file` | `path` and `bot_id` (its directory and its project's artifacts) or `project_id` (artifacts only) | `file` (`text` or `base64`, capped at 16 MiB) | | `list_decisions` | `project_id?, state?, tag?, bot_id?, query?, before?, limit?` | `decisions` | | `get_decision` | `decision_id` | `decision` (with comments, tags, notifications) | | `count_pending_decisions` | – | `pending_decisions` | @@ -174,6 +179,10 @@ breaking wire-shape change; v1 clients must upgrade before connecting. line changed. Sent when a finished turn becomes readable in the transcript, which lags the `ready` state; see Semantics. - `delivery_update`: `{ "delivery": {...} }`. +- `chat_turns`: `{ "bot_id", "turns": [...] }` — turns of a loaded chat that are new or + changed, usually the open one. Merge by turn `id`. Only bots whose chat a client has + listed are followed. The turn model is described in + [the chat pane design](superpowers/specs/2026-09-16-chat-pane-design.md). - `routine_run_update`: `{ "routine_run": {...} }`. - `approval_pending`: `{ "bot_id", "detail" }` — bot is waiting on its native permission prompt. - `notify`: `{ "level": "info|warn|error", "title", "body", "decision_id"? }` — diff --git a/docs/superpowers/specs/2026-09-16-chat-pane-design.md b/docs/superpowers/specs/2026-09-16-chat-pane-design.md index 79b601d..a6e67c3 100644 --- a/docs/superpowers/specs/2026-09-16-chat-pane-design.md +++ b/docs/superpowers/specs/2026-09-16-chat-pane-design.md @@ -1,7 +1,8 @@ # Chat pane: the default bot surface Issue: [ahilles107/gravity#13](https://github.com/ahilles107/gravity/issues/13). -Status: design. Branch: `codex/chat-pane` (stacked on `codex/peer-bots`). +Status: phase 1 (daemon chat model) implemented. Branch: `codex/chat-pane` +(stacked on `codex/peer-bots`). Opening a bot today shows its terminal. The terminal is the truth, but it is a poor way to read what a bot did. Images are invisible, diffs scroll past, files From 8538571dbccc869be28400510e3ebcb3ca67de3d Mon Sep 17 00:00:00 2001 From: Manuel Rinaldi Date: Thu, 1 Oct 2026 19:28:09 +0300 Subject: [PATCH 17/55] feat(desktop): open bots on a chat of their turns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 2 of the chat pane (ahilles107/gravity#13). Opening a bot now shows its conversation as turns read from the daemon's chat model: who woke it, its answer as markdown with highlighted code, tool steps folded into groups that expand to their command, diff and output, step images with a viewer, sent messages, results with their files, and decisions raised. The composer sends through send_user_message and shows a message's delivery state until the bot reads it. The chat follows chat_turns pushes and pages back through older turns. The Terminal and Routines tabs stay (⌘1-3, ⌘L focuses the composer), the side panel gains a Files tab listing the project's artifacts with previews, and a linked bot from a peer shows its bus conversation. A daemon without the chat capability keeps the terminal-first view. Bot markdown keeps the Control Center's policy: links and remote images are inert; images render only from bytes the daemon served. Also fixes list_messages paging, which sent before_id to a daemon reading before_num. Visual baselines for the new stories still need to come from CI. Co-Authored-By: Claude Opus 5.5 --- .../src/components/BotView.chat.test.tsx | 107 ++++++++ apps/desktop/src/components/BotView.test.tsx | 7 +- apps/desktop/src/components/BotView.tsx | 64 ++--- apps/desktop/src/components/bot/BotPanes.tsx | 96 +++++++ .../src/components/bot/BotSidePanel.tsx | 64 +++++ .../src/components/bot/BotTabs.stories.tsx | 2 +- apps/desktop/src/components/bot/BotTabs.tsx | 18 +- apps/desktop/src/components/bot/useBotKeys.ts | 34 +++ .../components/chat/ChatComposer.stories.tsx | 20 ++ .../src/components/chat/ChatComposer.tsx | 67 +++++ .../desktop/src/components/chat/ChatImage.tsx | 99 ++++++++ .../src/components/chat/ChatMarkdown.test.tsx | 29 +++ .../src/components/chat/ChatMarkdown.tsx | 45 ++++ .../src/components/chat/ChatPane.test.tsx | 206 +++++++++++++++ apps/desktop/src/components/chat/ChatPane.tsx | 101 ++++++++ .../src/components/chat/CodeBlock.stories.tsx | 29 +++ .../desktop/src/components/chat/CodeBlock.tsx | 52 ++++ apps/desktop/src/components/chat/DiffView.tsx | 32 +++ .../src/components/chat/LinkedChat.test.tsx | 61 +++++ .../src/components/chat/LinkedChat.tsx | 106 ++++++++ .../desktop/src/components/chat/StepGroup.tsx | 140 ++++++++++ .../src/components/chat/TurnView.stories.tsx | 123 +++++++++ apps/desktop/src/components/chat/TurnView.tsx | 119 +++++++++ .../src/components/chat/chatModel.test.ts | 105 ++++++++ apps/desktop/src/components/chat/chatModel.ts | 160 ++++++++++++ .../src/components/chat/highlight.test.ts | 48 ++++ apps/desktop/src/components/chat/highlight.ts | 137 ++++++++++ apps/desktop/src/components/chat/useChat.ts | 80 ++++++ .../desktop/src/components/chat/useDataUrl.ts | 67 +++++ .../src/components/chat/usePendingMessages.ts | 62 +++++ .../src/components/control/Markdown.tsx | 4 +- .../src/components/files/FilePreview.tsx | 106 ++++++++ .../src/components/files/FilesPanel.test.tsx | 129 ++++++++++ .../src/components/files/FilesPanel.tsx | 97 +++++++ apps/desktop/src/protocol/chat.ts | 135 ++++++++++ apps/desktop/src/protocol/entities.ts | 9 + apps/desktop/src/protocol/messages.ts | 30 ++- apps/desktop/src/protocol/push.ts | 4 + apps/desktop/src/protocol/requests.ts | 23 +- apps/desktop/src/protocol/wire.ts | 5 + apps/desktop/src/styles.css | 4 + apps/desktop/src/styles/chat-steps.css | 153 +++++++++++ apps/desktop/src/styles/chat.css | 239 ++++++++++++++++++ apps/desktop/src/styles/code.css | 90 +++++++ apps/desktop/src/styles/files.css | 86 +++++++ apps/desktop/src/test/chatFixtures.ts | 50 ++++ .../specs/2026-09-16-chat-pane-design.md | 3 +- 47 files changed, 3403 insertions(+), 44 deletions(-) create mode 100644 apps/desktop/src/components/BotView.chat.test.tsx create mode 100644 apps/desktop/src/components/bot/BotPanes.tsx create mode 100644 apps/desktop/src/components/bot/BotSidePanel.tsx create mode 100644 apps/desktop/src/components/bot/useBotKeys.ts create mode 100644 apps/desktop/src/components/chat/ChatComposer.stories.tsx create mode 100644 apps/desktop/src/components/chat/ChatComposer.tsx create mode 100644 apps/desktop/src/components/chat/ChatImage.tsx create mode 100644 apps/desktop/src/components/chat/ChatMarkdown.test.tsx create mode 100644 apps/desktop/src/components/chat/ChatMarkdown.tsx create mode 100644 apps/desktop/src/components/chat/ChatPane.test.tsx create mode 100644 apps/desktop/src/components/chat/ChatPane.tsx create mode 100644 apps/desktop/src/components/chat/CodeBlock.stories.tsx create mode 100644 apps/desktop/src/components/chat/CodeBlock.tsx create mode 100644 apps/desktop/src/components/chat/DiffView.tsx create mode 100644 apps/desktop/src/components/chat/LinkedChat.test.tsx create mode 100644 apps/desktop/src/components/chat/LinkedChat.tsx create mode 100644 apps/desktop/src/components/chat/StepGroup.tsx create mode 100644 apps/desktop/src/components/chat/TurnView.stories.tsx create mode 100644 apps/desktop/src/components/chat/TurnView.tsx create mode 100644 apps/desktop/src/components/chat/chatModel.test.ts create mode 100644 apps/desktop/src/components/chat/chatModel.ts create mode 100644 apps/desktop/src/components/chat/highlight.test.ts create mode 100644 apps/desktop/src/components/chat/highlight.ts create mode 100644 apps/desktop/src/components/chat/useChat.ts create mode 100644 apps/desktop/src/components/chat/useDataUrl.ts create mode 100644 apps/desktop/src/components/chat/usePendingMessages.ts create mode 100644 apps/desktop/src/components/files/FilePreview.tsx create mode 100644 apps/desktop/src/components/files/FilesPanel.test.tsx create mode 100644 apps/desktop/src/components/files/FilesPanel.tsx create mode 100644 apps/desktop/src/protocol/chat.ts create mode 100644 apps/desktop/src/styles/chat-steps.css create mode 100644 apps/desktop/src/styles/chat.css create mode 100644 apps/desktop/src/styles/code.css create mode 100644 apps/desktop/src/styles/files.css create mode 100644 apps/desktop/src/test/chatFixtures.ts diff --git a/apps/desktop/src/components/BotView.chat.test.tsx b/apps/desktop/src/components/BotView.chat.test.tsx new file mode 100644 index 0000000..023e071 --- /dev/null +++ b/apps/desktop/src/components/BotView.chat.test.tsx @@ -0,0 +1,107 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { turn } from "../test/chatFixtures"; +import { FakeDaemon } from "../test/fakeDaemon"; +import * as fx from "../test/fixtures"; +import { actionToastSpy, botSpy, routinesSpy, stubLocalStorage } from "../test/spies"; +import BotView from "./BotView"; + +vi.mock("./TerminalPane", () => ({ + default: (): React.ReactElement =>
, +})); + +function chatDaemon(): FakeDaemon { + const client = new FakeDaemon() + .onRequest("list_routines", () => ({ type: "routines", req_id: "1", routines: [] })) + .onRequest("list_chat", () => ({ + type: "chat", + req_id: "2", + bot_id: "b1", + has_more: false, + turns: [ + turn({ + items: [ + { + type: "completed", + id: "c1", + task_id: "t1", + result: "Ported.", + artifacts: [{ path: "/p/artifacts/report.md", name: "report.md" }], + }, + ], + }), + ], + })) + .onRequest("read_file", () => ({ + type: "file", + req_id: "3", + file: { name: "report.md", mime: "text/markdown", text: "Report body", truncated: false }, + })) + .onRequest("list_conversations", () => ({ + type: "conversations", + req_id: "4", + conversations: [], + })); + client.capabilities = ["terminal_attach", "chat"]; + return client; +} + +function renderView(client: FakeDaemon, bot = fx.bot()): void { + render( + , + ); +} + +describe("BotView with chat", () => { + beforeEach(() => { + stubLocalStorage(); + }); + + it("opens on the chat and keeps the terminal a tab away", async () => { + renderView(chatDaemon()); + expect(screen.getByRole("button", { name: "Chat" })).toHaveClass("tab-active"); + expect(await screen.findByText("Ported.")).toBeInTheDocument(); + expect(screen.getByTestId("terminal")).toBeInTheDocument(); + + fireEvent.keyDown(window, { key: "2", metaKey: true }); + expect(screen.getByRole("button", { name: "Terminal" })).toHaveClass("tab-active"); + + fireEvent.keyDown(window, { key: "l", metaKey: true }); + expect(screen.getByRole("button", { name: "Chat" })).toHaveClass("tab-active"); + await waitFor(() => { + expect(screen.getByRole("textbox", { name: "Message" })).toHaveFocus(); + }); + }); + + it("opens a result's file in the Files panel", async () => { + renderView(chatDaemon()); + await userEvent.click(await screen.findByRole("button", { name: "report.md" })); + expect(screen.getByRole("button", { name: "Files" })).toHaveClass("tab-active"); + expect(await screen.findByText("Report body")).toBeInTheDocument(); + }); + + it("gives a linked bot only its chat", () => { + renderView(chatDaemon(), fx.bot({ peer: { id: "x", name: "win-pc", online: true } })); + expect(screen.getByRole("button", { name: "Chat" })).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Terminal" })).not.toBeInTheDocument(); + expect(screen.queryByTestId("terminal")).not.toBeInTheDocument(); + }); + + it("falls back to the terminal on a daemon without chat", () => { + const client = chatDaemon(); + client.capabilities = ["terminal_attach"]; + renderView(client); + expect(screen.queryByRole("button", { name: "Chat" })).not.toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Terminal" })).toHaveClass("tab-active"); + }); +}); diff --git a/apps/desktop/src/components/BotView.test.tsx b/apps/desktop/src/components/BotView.test.tsx index ebe3924..36a5313 100644 --- a/apps/desktop/src/components/BotView.test.tsx +++ b/apps/desktop/src/components/BotView.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { fireEvent, render, screen, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { FakeDaemon } from "../test/fakeDaemon"; @@ -74,7 +74,10 @@ describe("BotView", () => { renderView(); expect(screen.getByTestId("terminal")).toBeInTheDocument(); expect(screen.getByRole("complementary", { name: "Bot info" })).toBeInTheDocument(); - expect(screen.queryByRole("button", { name: "Info" })).not.toBeInTheDocument(); + // Info is the side panel's tab, never one of the bot's own tabs. + const botTabs = screen.getByRole("button", { name: "Terminal" }).closest("nav"); + expect(botTabs).not.toBeNull(); + expect(within(botTabs ?? document.body).queryByRole("button", { name: "Info" })).toBeNull(); await user.click(screen.getByRole("button", { name: "Routines" })); expect(screen.getByText("Routines", { selector: ".panel-title" })).toBeInTheDocument(); diff --git a/apps/desktop/src/components/BotView.tsx b/apps/desktop/src/components/BotView.tsx index 5e68345..8e2301a 100644 --- a/apps/desktop/src/components/BotView.tsx +++ b/apps/desktop/src/components/BotView.tsx @@ -1,6 +1,5 @@ import { useEffect, useRef, useState } from "react"; import type { CSSProperties, KeyboardEvent, PointerEvent, ReactElement } from "react"; -import { isStopped } from "../app/bots"; import type { AddToast } from "../app/useToasts"; import type { DaemonApi } from "../protocol/api"; import type { Bot, Routine } from "../protocol/entities"; @@ -11,11 +10,12 @@ import { saveBotInfoPanel, } from "../settings"; import BotHeader from "./bot/BotHeader"; -import BotTabs from "./bot/BotTabs"; +import BotPanes from "./bot/BotPanes"; +import BotSidePanel from "./bot/BotSidePanel"; +import type { SideTab } from "./bot/BotSidePanel"; +import BotTabs, { botTabs } from "./bot/BotTabs"; import type { BotTab } from "./bot/BotTabs"; -import InfoPanel from "./InfoPanel"; -import RoutinesPanel from "./RoutinesPanel"; -import TerminalPane from "./TerminalPane"; +import { useBotKeys } from "./bot/useBotKeys"; interface BotViewProps { readonly client: DaemonApi; @@ -42,14 +42,21 @@ function clamp(value: number, minimum: number, maximum: number): number { export default function BotView(props: BotViewProps): ReactElement { const { client, bot, bots, connected, canControl, onToast } = props; - const [tab, setTab] = useState("terminal"); + const tabs = botTabs(client.capabilities.includes("chat"), bot.peer != null); + const [tab, setTab] = useState(tabs[0] ?? "terminal"); + const [side, setSide] = useState("info"); + const [openFile, setOpenFile] = useState(null); const [infoPanel, setInfoPanel] = useState(loadBotInfoPanel); const [maxInfoPanelWidth, setMaxInfoPanelWidth] = useState(FALLBACK_MAX_INFO_PANEL_WIDTH); const layoutRef = useRef(null); const dragRef = useRef(null); - // The terminal belongs to the user: any `control` connection may type while - // the bot runs. Bus deliveries arrive via the bot's inbox socket, never here. - const canWrite = canControl && !isStopped(bot); + useBotKeys(tabs, setTab); + + const showFile = (path: string): void => { + setSide("files"); + setOpenFile(path); + setInfoPanel((current) => ({ ...current, collapsed: false })); + }; useEffect(() => { saveBotInfoPanel(infoPanel); @@ -148,26 +155,19 @@ export default function BotView(props: BotViewProps): ReactElement { />
- -
- {/* Keep the terminal mounted across tab switches to preserve the buffer. */} -
- -
- {tab === "routines" ? ( -
- -
- ) : null} -
+ +
{infoPanel.collapsed ? null : (
- diff --git a/apps/desktop/src/components/bot/BotPanes.tsx b/apps/desktop/src/components/bot/BotPanes.tsx new file mode 100644 index 0000000..d58180d --- /dev/null +++ b/apps/desktop/src/components/bot/BotPanes.tsx @@ -0,0 +1,96 @@ +import type { ReactElement } from "react"; +import { isStopped } from "../../app/bots"; +import type { AddToast } from "../../app/useToasts"; +import type { DaemonApi } from "../../protocol/api"; +import type { Bot, Routine } from "../../protocol/entities"; +import ChatPane from "../chat/ChatPane"; +import LinkedChat from "../chat/LinkedChat"; +import RoutinesPanel from "../RoutinesPanel"; +import TerminalPane from "../TerminalPane"; +import type { BotTab } from "./BotTabs"; + +interface BotPanesProps { + readonly client: DaemonApi; + readonly bot: Bot; + readonly bots: readonly Bot[]; + readonly tabs: readonly BotTab[]; + readonly active: BotTab; + readonly connected: boolean; + readonly canControl: boolean; + readonly onOpenFile: (path: string) => void; + readonly onRoutinesChanged: (botId: string, routines: readonly Routine[]) => void; + readonly onToast: AddToast; +} + +/** Why the owner cannot write to a bot right now, or null when they can. */ +function writeBlockedReason(connected: boolean, canControl: boolean, bot: Bot): string | null { + if (!connected) { + return "Not connected to the daemon"; + } + if (!canControl) { + return "Read-only connection"; + } + return isStopped(bot) ? `${bot.name} is stopped` : null; +} + +function paneClass(shown: boolean): string { + return shown ? "tab-pane" : "tab-pane tab-pane-hidden"; +} + +/** + * The bot's tab bodies. Chat and terminal stay mounted when hidden, so + * switching back keeps the loaded conversation and the terminal buffer. + */ +export default function BotPanes(props: BotPanesProps): ReactElement { + const { client, bot, tabs, active, connected, canControl, onToast } = props; + const writeBlocked = writeBlockedReason(connected, canControl, bot); + const linked = bot.peer != null; + return ( +
+ {tabs.includes("chat") ? ( +
+ {linked ? ( + + ) : ( + + )} +
+ ) : null} + {tabs.includes("terminal") ? ( +
+ {/* The terminal belongs to the user: any `control` connection may type while the bot runs. */} + +
+ ) : null} + {active === "routines" ? ( +
+ +
+ ) : null} +
+ ); +} diff --git a/apps/desktop/src/components/bot/BotSidePanel.tsx b/apps/desktop/src/components/bot/BotSidePanel.tsx new file mode 100644 index 0000000..77a6a3e --- /dev/null +++ b/apps/desktop/src/components/bot/BotSidePanel.tsx @@ -0,0 +1,64 @@ +import type { ReactElement } from "react"; +import type { AddToast } from "../../app/useToasts"; +import type { DaemonApi } from "../../protocol/api"; +import type { Bot } from "../../protocol/entities"; +import FilesPanel from "../files/FilesPanel"; +import InfoPanel from "../InfoPanel"; + +export type SideTab = "info" | "files"; + +const SIDE_TABS: readonly SideTab[] = ["info", "files"]; + +interface BotSidePanelProps { + readonly client: DaemonApi; + readonly bot: Bot; + readonly connected: boolean; + readonly canControl: boolean; + readonly side: SideTab; + readonly onSide: (side: SideTab) => void; + readonly openFile: string | null; + readonly onOpenFile: (path: string | null) => void; + readonly onBotUpdated: (bot: Bot) => void; + readonly onToast: AddToast; +} + +/** The right-hand panel: the bot's info, or the project's files. */ +export default function BotSidePanel(props: BotSidePanelProps): ReactElement { + const { client, bot, connected, side, onSide } = props; + return ( + <> + + {side === "info" ? ( + + ) : ( + + )} + + ); +} diff --git a/apps/desktop/src/components/bot/BotTabs.stories.tsx b/apps/desktop/src/components/bot/BotTabs.stories.tsx index 90a8b98..0a25caf 100644 --- a/apps/desktop/src/components/bot/BotTabs.stories.tsx +++ b/apps/desktop/src/components/bot/BotTabs.stories.tsx @@ -5,5 +5,5 @@ import BotTabs from "./BotTabs"; export const Interactive: Story = () => { const [active, setActive] = useState("terminal"); - return ; + return ; }; diff --git a/apps/desktop/src/components/bot/BotTabs.tsx b/apps/desktop/src/components/bot/BotTabs.tsx index 3fbe431..8368705 100644 --- a/apps/desktop/src/components/bot/BotTabs.tsx +++ b/apps/desktop/src/components/bot/BotTabs.tsx @@ -1,26 +1,36 @@ import type { ReactElement } from "react"; -const BOT_TABS = ["terminal", "routines"] as const; -export type BotTab = (typeof BOT_TABS)[number]; +export type BotTab = "chat" | "terminal" | "routines"; const TAB_LABEL: Readonly> = { + chat: "Chat", terminal: "Terminal", routines: "Routines", }; +/** The tabs a bot offers: chat needs a daemon that serves it, and a linked bot has only chat. */ +export function botTabs(chat: boolean, linked: boolean): readonly BotTab[] { + if (linked) { + return ["chat"]; + } + return chat ? ["chat", "terminal", "routines"] : ["terminal", "routines"]; +} + interface BotTabsProps { + readonly tabs: readonly BotTab[]; readonly active: BotTab; readonly onSelect: (tab: BotTab) => void; } -export default function BotTabs({ active, onSelect }: BotTabsProps): ReactElement { +export default function BotTabs({ tabs, active, onSelect }: BotTabsProps): ReactElement { return (