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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -336,11 +336,11 @@ jobs:
test -f "$HOME/.orbit/embed/models/bge-small/orbit-model.json"

smoke-install-ubuntu:
name: Smoke test installer on Ubuntu 22.04
name: Smoke test installer on Ubuntu 24.04
needs: publish-release
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
container:
image: ubuntu:22.04
image: ubuntu:24.04
steps:
- name: Install prerequisites
shell: bash
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Paste the prompt below into your agent (Claude Code, Codex CLI, or Gemini CLI) *
> 3. Clone `https://github.com/danieljhkim/orbit` into the location from step 1, then run `make install`. This builds with cargo and copies the `orbit` binary to `$INSTALL_BIN_DIR` (default: `~/.cargo/bin`). Confirm the install path with me before running. Verify with `orbit --version`.
> 4. Run `orbit init` to initialize global state at `~/.orbit`.
> 5. From *this* repository (not the Orbit clone), run `orbit workspace init --mcp`. This creates `.orbit/` here and auto-registers Orbit's MCP server with installed agent CLIs (Claude Code, Codex, Gemini).
> 6. Ask me whether to enable semantic search (**optional**). `orbit semantic install` downloads a small embedder companion plus the default bge-small model (lives under `~/.orbit/embed/`) and powers `orbit search <query> --hybrid` / `orbit search similar <task-id>` over tasks. Don't install without my OK. If I accept and tasks already exist in this workspace, also run `orbit semantic index` to backfill the corpus.
> 6. Ask me whether to enable semantic search (**optional**). `orbit semantic install` downloads a small embedder companion plus the default bge-small model (lives under `~/.orbit/embed/`) and powers `orbit search <query> --hybrid` / `orbit search similar <task-id>` over tasks. It requires macOS arm64 or Linux x86_64/aarch64 with glibc >= 2.38; Intel macOS is unsupported for semantic search. Don't install without my OK. If I accept and tasks already exist in this workspace, also run `orbit semantic index` to backfill the corpus.
> 7. Read the key documents so you actually understand the model:
> - `README.md` — feature surface, install model, plugin vs CLI
> - `docs/POSITIONING.md` — what Orbit is for, what it isn't (especially "who this is for")
Expand Down Expand Up @@ -137,6 +137,11 @@ Customizing crews (which model runs planner/implementer/reviewer), the base bran

`orbit search` is the unified query surface for tasks, docs, learnings, and ADRs. It defaults to lexical matching. Opt into hybrid embedding ranking over task fields or indexed docs with `--hybrid`, or find cosine-neighbor tasks with `orbit search similar <task-id>`. The embedder runs as a separate companion subprocess, so semantic search has zero cost when unused.

The semantic companion is released for macOS arm64 and Linux x86_64/aarch64
with glibc >= 2.38 (Ubuntu 24.04 or equivalent). Intel macOS can run the
Orbit CLI, but semantic search is unsupported because there is no Intel macOS
companion asset.

```bash
orbit semantic install # one-time: download companion + default model (bge-small)
orbit semantic index # backfill existing tasks
Expand Down
4 changes: 4 additions & 0 deletions crates/orbit-core/assets/skills/orbit-search/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ Do not use search for "find every symbol matching pattern X"; use `orbit.graph.s
| Show status | CLI-only | `orbit semantic stats` |
| Rebuild embeddings | CLI-only | `orbit semantic index --kind tasks|docs|learnings|adrs|all [--model MODEL] [--force]` |

The released semantic companion supports macOS arm64 and Linux x86_64/aarch64
with glibc >= 2.38. Intel macOS can run the CLI, but semantic search is
unsupported because there is no x86_64-apple-darwin companion asset.

Do not run `install` without operator consent. If a semantic query fails because the companion is missing, fall back to plain `orbit search --kind <kind> <query>` (lexical) and continue unless the user explicitly asked to enable embeddings.

## Result Shape
Expand Down
13 changes: 10 additions & 3 deletions crates/orbit-search/src/commands/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ use sha2::{Digest, Sha256};

use crate::commands::{DEFAULT_RELEASE_BASE_URL, parse_model};
use crate::companion::{
COMPANION_OVERRIDE_ENV, UNSAFE_COMPANION_OVERRIDE_ENV, unsafe_companion_overrides_enabled,
validate_companion_override_path, validate_managed_companion_path,
COMPANION_OVERRIDE_ENV, UNSAFE_COMPANION_OVERRIDE_ENV, ensure_semantic_search_supported,
unsafe_companion_overrides_enabled, validate_companion_override_path,
validate_managed_companion_path,
};
use crate::{CompanionPaths, platform_companion_filename};

Expand Down Expand Up @@ -203,7 +204,13 @@ pub(crate) fn resolve_download_source() -> Result<CompanionDownloadSource, Orbit
)));
}

let asset_name = platform_companion_filename();
ensure_semantic_search_supported()?;
default_release_download_source(platform_companion_filename())
}

pub(crate) fn default_release_download_source(
asset_name: String,
) -> Result<CompanionDownloadSource, OrbitError> {
let url = format!("{DEFAULT_RELEASE_BASE_URL}/{asset_name}");
validate_download_url(&url)?;
Ok(CompanionDownloadSource {
Expand Down
48 changes: 43 additions & 5 deletions crates/orbit-search/src/commands/tests/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
use super::super::install::path_execution_fallback_rationale;
use super::super::install::{
CompanionIntegrity, CompanionLaunchMode, ManagedCompanion, SemanticInstallParams,
checksum_from_manifest, companion_launch_mode, resolve_download_source, run, sha256_hex,
verify_release_checksum_signature_with_key,
checksum_from_manifest, companion_launch_mode, default_release_download_source, run,
sha256_hex, verify_release_checksum_signature_with_key,
};

use crate::companion::unsafe_companion_overrides_enabled;
use crate::companion::{
ensure_semantic_search_supported_for_platform, unsafe_companion_overrides_enabled,
};
use crate::{CompanionPaths, locate_companion, platform_companion_filename};
use std::path::PathBuf;
use std::sync::{Mutex, MutexGuard, OnceLock};
Expand Down Expand Up @@ -190,6 +192,41 @@ fn companion_launch_mode_matches_platform_support() {
}
}

#[test]
fn semantic_install_rejects_intel_mac_platform_before_download() {
let error = ensure_semantic_search_supported_for_platform("macos-x86_64", None)
.expect_err("Intel macOS should not have a released semantic companion");

assert!(
error
.to_string()
.contains("semantic search unsupported on this platform (macos-x86_64)"),
"{error}"
);
}

#[test]
fn semantic_install_accepts_released_companion_platforms() {
ensure_semantic_search_supported_for_platform("macos-aarch64", None)
.expect("macOS arm64 companion is released");
ensure_semantic_search_supported_for_platform("linux-x86_64", Some("2.38"))
.expect("Linux x86_64 companion is released on glibc 2.38+");
ensure_semantic_search_supported_for_platform("linux-aarch64", Some("2.39"))
.expect("Linux aarch64 companion is released on glibc 2.38+");
}

#[test]
fn semantic_install_rejects_linux_glibc_below_floor() {
let error = ensure_semantic_search_supported_for_platform("linux-x86_64", Some("2.35"))
.expect_err("glibc 2.35 should be below the companion runtime floor");

assert!(
error.to_string().contains("requires glibc >= 2.38"),
"{error}"
);
assert!(error.to_string().contains("detected glibc 2.35"), "{error}");
}

#[test]
#[cfg(unix)]
fn checksum_mismatch_rejects_replacement_before_install() {
Expand Down Expand Up @@ -253,7 +290,8 @@ fn default_download_source_requires_release_checksum_manifest() {
remove_env("ORBIT_SEARCH_COMPANION_SHA256");
remove_env("ORBIT_SEARCH_COMPANION_ALLOW_UNSAFE");

let source = resolve_download_source().expect("default source");
let source = default_release_download_source("orbit-search-companion-linux-x86_64".to_string())
.expect("default source");

match source.integrity {
CompanionIntegrity::ReleaseSignedChecksum {
Expand All @@ -263,7 +301,7 @@ fn default_download_source_requires_release_checksum_manifest() {
} => {
assert!(checksums_url.ends_with("/orbit-checksums.txt"));
assert!(signature_url.ends_with("/orbit-checksums.txt.sig"));
assert_eq!(asset_name, platform_companion_filename());
assert_eq!(asset_name, "orbit-search-companion-linux-x86_64");
}
other => panic!("default source should require signed release checksum: {other:?}"),
}
Expand Down
74 changes: 74 additions & 0 deletions crates/orbit-search/src/companion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
//! `CompanionNotInstalled` shape so callers can surface a clean install hint.

use std::env;
#[cfg(all(target_os = "linux", target_env = "gnu"))]
use std::ffi::CStr;
use std::fs;
use std::path::{Path, PathBuf};

Expand All @@ -15,6 +17,9 @@ use orbit_common::types::OrbitError;
pub(crate) const COMPANION_OVERRIDE_ENV: &str = "ORBIT_SEARCH_COMPANION";
pub(crate) const UNSAFE_COMPANION_OVERRIDE_ENV: &str = "ORBIT_SEARCH_COMPANION_ALLOW_UNSAFE";
pub const INSTALL_REMEDIATION: &str = "Semantic search not enabled. Run `orbit semantic install` to download the inference companion.";
const MIN_LINUX_GLIBC_MAJOR: u32 = 2;
const MIN_LINUX_GLIBC_MINOR: u32 = 38;
const MIN_LINUX_GLIBC_DISPLAY: &str = "2.38";

#[derive(Debug, Clone)]
pub struct CompanionPaths {
Expand Down Expand Up @@ -70,6 +75,75 @@ pub fn platform_id() -> &'static str {
}
}

pub(crate) fn ensure_semantic_search_supported() -> Result<(), OrbitError> {
let glibc_version = current_glibc_version();
ensure_semantic_search_supported_for_platform(platform_id(), glibc_version.as_deref())
}

pub(crate) fn ensure_semantic_search_supported_for_platform(
platform: &str,
glibc_version: Option<&str>,
) -> Result<(), OrbitError> {
match platform {
"macos-aarch64" => Ok(()),
"linux-aarch64" | "linux-x86_64" => ensure_linux_glibc_supported(platform, glibc_version),
_ => Err(unsupported_semantic_platform(platform)),
}
}

fn ensure_linux_glibc_supported(
platform: &str,
glibc_version: Option<&str>,
) -> Result<(), OrbitError> {
let Some(glibc_version) = glibc_version else {
return Ok(());
};
let Some((major, minor)) = parse_glibc_major_minor(glibc_version) else {
return Ok(());
};
if (major, minor) < (MIN_LINUX_GLIBC_MAJOR, MIN_LINUX_GLIBC_MINOR) {
return Err(OrbitError::InvalidInput(format!(
"semantic search unsupported on this Linux system ({platform}): orbit-search-companion requires glibc >= {MIN_LINUX_GLIBC_DISPLAY}; detected glibc {glibc_version}. Use Ubuntu 24.04 or another Linux distribution with glibc >= {MIN_LINUX_GLIBC_DISPLAY}."
)));
}
Ok(())
}

fn unsupported_semantic_platform(platform: &str) -> OrbitError {
OrbitError::InvalidInput(format!(
"semantic search unsupported on this platform ({platform}); supported companion platforms are macOS arm64 and Linux x86_64/aarch64 with glibc >= {MIN_LINUX_GLIBC_DISPLAY}"
))
}

fn parse_glibc_major_minor(version: &str) -> Option<(u32, u32)> {
let mut pieces = version.split('.');
let major = pieces.next()?.parse::<u32>().ok()?;
let minor = pieces.next()?.parse::<u32>().ok()?;
Some((major, minor))
}

#[cfg(all(target_os = "linux", target_env = "gnu"))]
fn current_glibc_version() -> Option<String> {
let version = {
// SAFETY: gnu_get_libc_version returns a process-static NUL-terminated
// string pointer, or null on failure; we only read it when non-null.
unsafe { libc::gnu_get_libc_version() }
};
if version.is_null() {
return None;
}
let version = {
// SAFETY: non-null gnu_get_libc_version result points at a valid C string.
unsafe { CStr::from_ptr(version) }
};
version.to_str().ok().map(str::to_string)
}

#[cfg(not(all(target_os = "linux", target_env = "gnu")))]
fn current_glibc_version() -> Option<String> {
None
}

pub fn locate_companion() -> Result<PathBuf, OrbitError> {
if let Ok(paths) = CompanionPaths::default_under_home() {
let standard = paths.companion_path();
Expand Down
11 changes: 10 additions & 1 deletion docs/RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,16 @@ Each step names the exact file or command. Do them in order.
- `smoke-install-macos` — installs the tagged macOS arm64 CLI, then runs
`orbit semantic install --json` with isolated runtime state.
- `smoke-install-ubuntu` — installs the tagged Linux x86_64 CLI, then runs
`orbit semantic install --json` with isolated runtime state.
`orbit semantic install --json` with isolated runtime state inside an
Ubuntu 24.04 container.

The Linux CLI release binaries still build on Ubuntu 22.04 to keep the CLI
runtime floor low. The semantic companion is stricter: ONNX Runtime requires
glibc >= 2.38 at runtime, so Linux companion builds and semantic smoke tests
use Ubuntu 24.04 or newer. Released semantic companion assets currently
cover macOS arm64 and Linux x86_64/aarch64 with glibc >= 2.38. Intel macOS
receives a CLI asset, but semantic search is unsupported because the
companion has no x86_64-apple-darwin ONNX Runtime prebuilt.

All five must be green before step 7.

Expand Down
Loading