Skip to content
Closed
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
25 changes: 18 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 5 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ octos-core = { git = "https://github.com/octos-org/octos.git", rev = "a6ea850517
octos-cli = { git = "https://github.com/octos-org/octos.git", rev = "a6ea8505170735f191a12bb47629a6728415d417", default-features = false }
# --- Rinx (Matrix chats, Moments, articles, mini apps) as a hosted module:
# the library only, never its standalone entry or its own kernel (Rinx ADR 0007).
rinx = { git = "https://github.com/hagency-org/Rinx.git", rev = "a72e4b003a05ebcb31bc86c346d7dd06cdffbceb", default-features = false, features = ["octosense-module"] }
rinx = { git = "https://github.com/hagency-org/Rinx.git", rev = "197d5170892d64046ba8e291e80aa326c4e4c87a", default-features = false, features = ["octosense-module"] }
# --- AppCard's inline renderers.
streaming-markdown-kit = { git = "https://github.com/Project-Robius-China/streaming-markdown-kit.git", branch = "main" }
makepad-diagram-kit = { git = "https://github.com/Project-Robius-China/makepad-diagram-kit.git", branch = "main" }
Expand Down Expand Up @@ -208,12 +208,11 @@ octoscript-makepad = { path = ".sources/octoscript-makepad/crates/octoscript-mak
octoscript-widgets = { path = ".sources/octoscript-makepad/crates/octoscript-widgets" }
octoscript-node = { path = ".sources/octoscript-makepad/crates/octoscript-node" }

# Rinx still takes the app-peers contract by git from OctoSense-System-Apps
# (archived; it was imported into this repository): that code lives here
# now, so these are the same crates this workspace links — one broker
# contract and one kernel.
[patch."https://github.com/OctoSense-org/OctoSense-System-Apps.git"]
# Hosted Rinx uses this workspace's broker/config/kernel, including when
# its standalone workspace pins an earlier OctoSense revision.
[patch."https://github.com/OctoSense-org/OctoSense.git"]
octosense-app-peers = { path = "crates/app-peers" }
octosense-kernel = { path = "crates/kernel" }
octosense-llm-config = { path = "apps/ai-providers/config" }

# Rinx's Matrix stack: the patched ruma its own workspace uses (Rinx's
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Pinned exactly once, in the root `Cargo.toml` and the runtime locks:
| [makepad (OctoSense fork)](https://github.com/OctoSense-org/makepad) | The UI framework and the `cargo-makepad` packager. Checked out in `.sources/makepad`, plus the reviewed runtime patch. |
| [OctoScript-Makepad](https://github.com/OctoSense-org/OctoScript-Makepad), [OctoScript](https://github.com/OctoSense-org/OctoScript) | The runtime release that names the Makepad and OctoScript revisions (`native-runtime.lock.json`). |
| [OctoSense-App-Hub](https://github.com/OctoSense-org/OctoSense-App-Hub) | The signed catalog, the store, the Card runner that contains every app (`octosense-app-hub-app`). |
| [octos](https://github.com/octos-org/octos) | The agent kernel. On Android the APK bundles it as `liboctos.so`; on a desktop the kernel service runs the binary named by `OCTOS_APP_CORE_BIN`. |
| [octos](https://github.com/octos-org/octos) | The agent kernel. On Android the APK bundles it as `liboctos.so`; on a desktop it finds the packaged `octos-kernel` beside the shell; `OCTOS_APP_CORE_BIN` remains an override. |
| [Rinx](https://github.com/hagency-org/Rinx) | Matrix chats and mini apps, hosted as a native module. |

Related, not build inputs: [OctoScript-App-Design-Flow](https://github.com/OctoSense-org/OctoScript-App-Design-Flow) (how apps are built and published), [OctoScript-Android](https://github.com/OctoSense-org/OctoScript-Android) and [OctoScript-OH](https://github.com/OctoSense-org/OctoScript-OH) (other renderers), the [OctoSense website](https://github.com/OctoSense-org/octosense-org.github.io).
Expand Down Expand Up @@ -70,6 +70,8 @@ cargo check --locked -p octosense --features mobile-apps
cargo check --locked -p octosense -p octosense-appcard --features mobile-apps,app-appcard
```

For the desktop AI runtime, `tools/build-desktop.py` builds and stages the locked kernel beside the shell. See [desktop build instructions and validation limits](desktop/README.md#build-and-run). Cargo alone does not install the kernel.

**Phone** (from `phone/`, which selects the phone's system apps; details in [phone/README.md](phone/README.md)):

```sh
Expand Down
4 changes: 3 additions & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Shell 只有一份,位于 `crates/shell`([ADR 0001(英文)](docs/adr/000
| [makepad(OctoSense fork)](https://github.com/OctoSense-org/makepad) | UI 框架和 `cargo-makepad` 打包工具。检出到 `.sources/makepad`,并应用经审查的运行时补丁。 |
| [OctoScript-Makepad](https://github.com/OctoSense-org/OctoScript-Makepad)、[OctoScript](https://github.com/OctoSense-org/OctoScript) | 指定 Makepad 和 OctoScript 版本的运行时发行版(`native-runtime.lock.json`)。 |
| [OctoSense-App-Hub](https://github.com/OctoSense-org/OctoSense-App-Hub) | 签名目录、商店,以及隔离运行每个应用的 Card runner(`octosense-app-hub-app`)。 |
| [octos](https://github.com/octos-org/octos) | Agent 内核。在 Android 上 APK 以 `liboctos.so` 形式内置它;在桌面上内核服务运行 `OCTOS_APP_CORE_BIN` 指定的程序。 |
| [octos](https://github.com/octos-org/octos) | Agent 内核。在 Android 上 APK 以 `liboctos.so` 形式内置它;在桌面上自动发现 Shell 程序旁的 `octos-kernel`;`OCTOS_APP_CORE_BIN` 仍可显式覆盖。 |
| [Rinx](https://github.com/hagency-org/Rinx) | Matrix 聊天与小程序,作为原生模块托管。 |

相关但不参与构建:[OctoScript-App-Design-Flow](https://github.com/OctoSense-org/OctoScript-App-Design-Flow)(如何构建和发布应用)、[OctoScript-Android](https://github.com/OctoSense-org/OctoScript-Android) 和 [OctoScript-OH](https://github.com/OctoSense-org/OctoScript-OH)(其他渲染后端)、[OctoSense 网站](https://github.com/OctoSense-org/octosense-org.github.io)。
Expand Down Expand Up @@ -70,6 +70,8 @@ cargo check --locked -p octosense --features mobile-apps
cargo check --locked -p octosense -p octosense-appcard --features mobile-apps,app-appcard
```

桌面 AI 运行时由 `tools/build-desktop.py` 按锁定版本构建并放到程序旁;具体步骤和已验证范围见 [桌面构建说明](desktop/README.zh-CN.md#构建与运行)。独立执行 Cargo 不会安装内核。

**手机端**(在 `phone/` 中运行,它会选择手机端的系统应用;详见 [phone/README.zh-CN.md](phone/README.zh-CN.md)):

```sh
Expand Down
2 changes: 1 addition & 1 deletion crates/ai-host/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ ai_host::shutdown();
```

`Host` fields: `data_dir`; `kernel: KernelSource` (`Bundled` on Android,
`InProcess` on OpenHarmony, `Env` = `$OCTOS_APP_CORE_BIN` on a desktop,
`InProcess` on OpenHarmony, `Env` = `$OCTOS_APP_CORE_BIN` or the packaged sibling on a desktop,
`Program(path)`, `None`; `KernelSource::platform()` picks); `qr_import:
QrImport` (`platform()` or `paste_only()`); `policy: Policy`
(`Policy::shipped()` grants Rinx the `octos.*` services).
Expand Down
6 changes: 3 additions & 3 deletions crates/ai-host/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ pub enum KernelSource {
Bundled,
/// OpenHarmony: the canonical core linked in-process.
InProcess,
/// Desktop: the binary `$OCTOS_APP_CORE_BIN` names; none without it (a
/// developer's own `octos serve` is never touched).
/// Desktop: `$OCTOS_APP_CORE_BIN`, or the packaged sibling executable.
/// The name stays compatible; no developer kernel is attached to.
Env,
/// Desktop or Android: this binary.
Program(PathBuf),
Expand All @@ -76,7 +76,7 @@ pub enum KernelSource {

impl KernelSource {
/// This platform's kernel: bundled on Android, in-process on
/// OpenHarmony, `$OCTOS_APP_CORE_BIN` on a desktop, none on iOS.
/// OpenHarmony, override/packaged sibling on a desktop, none on iOS.
pub fn platform() -> Self {
if cfg!(target_os = "android") {
KernelSource::Bundled
Expand Down
5 changes: 3 additions & 2 deletions crates/kernel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ How it starts, per platform (`src/launch.rs`):
- **Desktop**: `<program> serve --stdio --data-dir <core_dir>` (plus
`--config <core_dir>/config.json` when that file exists) with
`OCTOS_HOME=<core_dir>`; the program is the shell's `Options::program` or
`$OCTOS_APP_CORE_BIN`. With neither there is no kernel: a developer's own
`octos serve` is never touched.
`$OCTOS_APP_CORE_BIN`, falling back to `octos-kernel[.exe]` beside the shell
executable. `tools/build-desktop.py` stages the locked runtime there. No
PATH search or attachment to another running kernel occurs.
- **iOS**: no kernel.

The kernel and the frame pump run on the crate's own Tokio runtime, so a
Expand Down
57 changes: 57 additions & 0 deletions crates/kernel/examples/packaged_probe.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//! Offline verification of a packaged sibling, without a program override.
//! Run beside `octos-kernel`; uses a new temporary profile, no provider calls.
use std::{collections::BTreeMap, path::Path, time::{Duration, SystemTime, UNIX_EPOCH}};
use octosense_kernel::{CloseReason, Connection, Core, Options};
use octosense_llm_config::{profile, Provider, ProviderSet};
use serde_json::{json, Value};

async fn call(conn: &mut Connection, id: &str, method: &str, params: Value) -> Value {
conn.send(json!({"jsonrpc":"2.0", "id":id, "method":method, "params":params}).to_string()).unwrap();
loop {
let text = tokio::time::timeout(Duration::from_secs(30), conn.recv()).await.unwrap().unwrap();
let reply: Value = serde_json::from_str(&text).unwrap();
if reply["id"] == id {
assert!(reply.get("error").is_none(), "{reply}");
return reply["result"].clone();
}
}
}

fn provider(root: &Path, family: &str, model: &str) {
let primary = Provider::new(family, Some(model.into()));
let env = BTreeMap::from([(primary.key_env.clone(), "test-fixture-not-a-real-key".into())]);
profile::save_merge(&profile::profile_path(root), &ProviderSet {primary: Some(primary), fallbacks: vec![]}, &env).unwrap();
}

#[tokio::main]
async fn main() {
assert!(std::env::var_os("OCTOS_APP_CORE_BIN").is_none(), "probe requires automatic discovery");
let root = std::env::temp_dir().join(format!("octos-packaged-{}-{}", std::process::id(), SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos()));
std::fs::create_dir(&root).unwrap();
let core = Core::new(Options::default().core_dir(&root));
struct Cleanup(Core, std::path::PathBuf);
impl Drop for Cleanup {
fn drop(&mut self) {
self.0.shutdown_within(Duration::from_secs(5));
let _ = std::fs::remove_dir_all(&self.1);
}
}
let _cleanup = Cleanup(core.clone(), root.clone());
provider(&root, "deepseek", "deepseek-v4-flash");
let mut first = core.connect().expect("packaged runtime must be found without an override");
let mut second = core.connect().unwrap();
assert_eq!(first.generation(), second.generation());
let list = call(&mut first, "providers", "profile/llm/list", json!({"profile_id":"_main"})).await;
assert_eq!(list["primary"]["model"], "deepseek-v4-flash");
let list = call(&mut second, "providers", "profile/llm/list", json!({"profile_id":"_main"})).await;
assert_eq!(list["primary"]["model"], "deepseek-v4-flash");
provider(&root, "moonshot", "kimi-k2.5");
assert!(core.restart());
assert_eq!(first.recv().await, Err(CloseReason::Restarted));
assert_eq!(second.recv().await, Err(CloseReason::Restarted));
let mut next = core.connect().unwrap();
assert_eq!(next.generation(), 2);
let list = call(&mut next, "providers", "profile/llm/list", json!({"profile_id":"_main"})).await;
assert_eq!(list["primary"]["model"], "kimi-k2.5");
println!("PASS: sibling runtime, shared kernel, provider profile and restart; no LLM request");
}
84 changes: 71 additions & 13 deletions crates/kernel/src/launch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
//! (`octos_cli::embedded::serve_io`); HAP native libraries cannot exec.
//! - **Desktop**: `<program> serve --stdio --data-dir <core_dir> --config
//! <core_dir>/config.json` with `OCTOS_HOME=<core_dir>`, where the program
//! is the shell's [`crate::Options::program`] or `$OCTOS_APP_CORE_BIN`.
//! Without one there is no kernel (a developer's own `octos serve` is never
//! touched).
//! is the shell's [`crate::Options::program`], `$OCTOS_APP_CORE_BIN`, or
//! the packaged `octos-kernel[.exe]` beside the shell executable, in that
//! order. No PATH search or connection to another running kernel occurs.
//! - **iOS**: no kernel (an app cannot exec a child).

use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -96,20 +96,36 @@ pub(crate) fn resolve(inputs: &Inputs) -> Result<Launch, Unavailable> {
}
#[cfg(not(any(target_env = "ohos", target_os = "ios", target_os = "android")))]
{
let program = match inputs.program {
Some(p) => p.to_path_buf(),
None => std::env::var_os("OCTOS_APP_CORE_BIN")
.filter(|v| !v.is_empty())
.map(PathBuf::from)
.ok_or_else(|| Unavailable::NoKernel("no kernel binary configured (OCTOS_APP_CORE_BIN)".into()))?,
};
if !program.is_file() {
return Err(Unavailable::NoKernel(format!("{} is not a file", program.display())));
}
let env = std::env::var_os("OCTOS_APP_CORE_BIN").filter(|v| !v.is_empty()).map(PathBuf::from);
let executable = std::env::current_exe().ok();
let program = desktop_program(inputs.program, env.as_deref(), executable.as_deref())?;
Ok(desktop_stdio(program, core_dir, inputs.env))
}
}

/// Overrides are authoritative: a broken override must not silently choose
/// another version. The sibling travels with the shell, independent of cwd.
#[cfg(not(any(target_env = "ohos", target_os = "ios", target_os = "android")))]
fn desktop_program(explicit: Option<&Path>, env: Option<&Path>, executable: Option<&Path>) -> Result<PathBuf, Unavailable> {
let packaged = executable.and_then(Path::parent).map(|dir| {
dir.join(if cfg!(windows) { "octos-kernel.exe" } else { "octos-kernel" })
});
let program = explicit.or(env).or(packaged.as_deref()).ok_or_else(|| {
Unavailable::NoKernel("no packaged kernel or OCTOS_APP_CORE_BIN override".into())
})?;
if !program.is_file() {
return Err(Unavailable::NoKernel(format!("{} is not a file; build the desktop runtime with tools/build-desktop.py", program.display())));
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if std::fs::metadata(program).map(|m| m.permissions().mode() & 0o111 == 0).unwrap_or(true) {
return Err(Unavailable::NoKernel(format!("{} is not executable", program.display())));
}
}
Ok(program.to_owned())
}

/// The desktop launch: an explicit data dir, as AppCard's local core mode has
/// always run it, and `<core_dir>/config.json` when there is one (octos
/// refuses to start on a named config file that does not exist; without one
Expand Down Expand Up @@ -338,6 +354,48 @@ mod tests {
let _ = std::fs::remove_dir_all(home);
}

#[cfg(not(any(target_env = "ohos", target_os = "ios", target_os = "android")))]
#[test]
fn desktop_finds_its_sibling_and_keeps_overrides_authoritative() {
let dir = tmp("packaged");
let shell = dir.join("octosense");
let bundled = dir.join(if cfg!(windows) { "octos-kernel.exe" } else { "octos-kernel" });
let explicit = dir.join("explicit");
let env = dir.join("environment");
for p in [&bundled, &explicit, &env] {
std::fs::write(p, "fixture").unwrap();
#[cfg(unix)] {
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(p, std::fs::Permissions::from_mode(0o755)).unwrap();
}
}
assert_eq!(desktop_program(None, None, Some(&shell)).unwrap(), bundled);
assert_eq!(desktop_program(None, Some(&env), Some(&shell)).unwrap(), env);
assert_eq!(desktop_program(Some(&explicit), Some(&env), Some(&shell)).unwrap(), explicit);
let missing = dir.join("missing");
assert!(desktop_program(Some(&missing), Some(&env), Some(&shell)).is_err());
assert!(desktop_program(None, Some(&missing), Some(&shell)).is_err());
// Discovery follows the installed executable after a directory move.
let moved = dir.with_extension("moved");
std::fs::rename(&dir, &moved).unwrap();
let sibling = moved.join(bundled.file_name().unwrap());
assert_eq!(desktop_program(None, None, Some(&moved.join("octosense"))).unwrap(), sibling);
std::fs::remove_file(sibling).unwrap();
assert!(desktop_program(None, None, Some(&moved.join("octosense"))).is_err());
assert!(desktop_program(None, None, None).is_err());
std::fs::remove_dir_all(moved).unwrap();
}

#[cfg(all(unix, not(any(target_env = "ohos", target_os = "ios", target_os = "android"))))]
#[test]
fn desktop_rejects_a_nonexecutable_runtime() {
let dir = tmp("not-executable");
let program = dir.join("octos-kernel");
std::fs::write(&program, "fixture").unwrap();
assert!(desktop_program(None, None, Some(&dir.join("octosense"))).is_err());
std::fs::remove_dir_all(dir).unwrap();
}

#[cfg(not(any(target_env = "ohos", target_os = "ios", target_os = "android")))]
#[test]
fn desktop_without_a_program_has_no_kernel() {
Expand Down
3 changes: 2 additions & 1 deletion crates/kernel/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ impl Options {
self
}
/// The kernel binary (desktop; on Android it overrides the bundled
/// `liboctos.so`). Default: `$OCTOS_APP_CORE_BIN`.
/// `liboctos.so`). Desktop default: `$OCTOS_APP_CORE_BIN`, then the
/// packaged `octos-kernel[.exe]` beside this process's executable.
pub fn program(mut self, program: impl Into<PathBuf>) -> Self {
self.program = Some(program.into());
self
Expand Down
Loading
Loading