From 5f2db0cbb8aef19081d1827ec8d5ddc275248363 Mon Sep 17 00:00:00 2001
From: ymote <151983+ymote@users.noreply.github.com>
Date: Thu, 1 Oct 2026 09:48:40 -0700
Subject: [PATCH] Desktop: find the packaged octos kernel and refuse one of
another revision
The desktop's kernel service needed OCTOS_APP_CORE_BIN. It now also finds
a packaged `octos-kernel[.exe]` beside the shell executable (in a macOS .app,
Contents/MacOS or Contents/Resources), never in the working directory or on
PATH. Options::program and OCTOS_APP_CORE_BIN still win, unchecked, and a
broken override is an error rather than a fallback.
The packaged kernel runs only when its receipt `octos-kernel.json` names the
octos revision this build pins (crates/kernel/build.rs reads it from
Cargo.lock) and the binary's SHA-256 matches. A stale
target/release/octos-kernel from an earlier pin, a missing receipt or a
swapped binary is refused, and the reason reaches the person.
OCTOSENSE_KERNEL_ANY_REVISION=1 accepts another revision while developing;
the receipt and hash are still required. Resolving reads only the receipt
(it also answers status queries on the UI thread); the hash is checked in
launch::prepare on the kernel's runtime before each start, cached by size
and modification time.
tools/kernel-artifact.py --host --stage
builds the pinned kernel and
stages it with that receipt, refusing a binary whose --version is not the
pinned revision (--kernel stages a prebuilt one). The host build passes
--target-dir instead of an `env` wrapper, so it runs on Windows too.
Replaces the kernel part of #85. Its Rinx pin and [patch] changes are
dropped: main pins Rinx v1.1.0 and the app contract from crates.io.
Co-Authored-By: Claude Opus 5.5 (1M context)
Co-Authored-By: Claude Opus 5.5
---
Cargo.lock | 1 +
README.md | 8 +-
README.zh-CN.md | 8 +-
crates/ai-host/README.md | 3 +-
crates/ai-host/src/lib.rs | 7 +-
crates/kernel/Cargo.toml | 5 +
crates/kernel/README.md | 13 +-
crates/kernel/README.zh-CN.md | 10 +-
crates/kernel/build.rs | 58 +++++
crates/kernel/src/launch.rs | 426 ++++++++++++++++++++++++++++++++--
crates/kernel/src/lib.rs | 5 +-
crates/kernel/tests/core.rs | 9 +-
desktop/README.md | 17 +-
desktop/README.zh-CN.md | 17 +-
docs/ai-services.md | 8 +-
docs/ai-services.zh-CN.md | 8 +-
docs/architecture.md | 4 +-
docs/architecture.zh-CN.md | 4 +-
tools/kernel-artifact.py | 72 +++++-
tools/test_kernel_artifact.py | 77 ++++++
20 files changed, 702 insertions(+), 58 deletions(-)
create mode 100644 crates/kernel/build.rs
diff --git a/Cargo.lock b/Cargo.lock
index d496f1ad..a5519168 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -7581,6 +7581,7 @@ dependencies = [
"octos-core",
"octosense-llm-config",
"serde_json",
+ "sha2 0.10.9",
"tokio",
"tokio-tungstenite 0.24.0",
"tracing-subscriber",
diff --git a/README.md b/README.md
index 8bfbf1fb..5858307f 100644
--- a/README.md
+++ b/README.md
@@ -66,7 +66,7 @@ flowchart LR
- **The shell** (`crates/shell`, one process) hosts the window manager, the native modules (App Hub, Rinx), App Hub's Card runner (every script app in its own isolate), the system chat, the approval router, the host-tool relay and [`crates/ai-host`](crates/ai-host/README.md), whose [app-peers broker](crates/app-peers/README.md) is the kernel's host connection.
-- **The octos kernel** ([`crates/kernel`](crates/kernel/README.md)) starts on first use: a child process speaking OUP over stdio on the desktop (`OCTOS_APP_CORE_BIN`) and Android (`liboctos.so`), an in-process task on OpenHarmony, none on iOS. It exits with the shell.
+- **The octos kernel** ([`crates/kernel`](crates/kernel/README.md)) starts on first use: a child process speaking OUP over stdio on the desktop (the packaged `octos-kernel` beside the shell, or `OCTOS_APP_CORE_BIN`) and Android (`liboctos.so`), an in-process task on OpenHarmony, none on iOS. It exits with the shell.
- **Process apps**: on the desktop the Terminal runs as its own process, attached over the shell's hub (frames and the AI bus), in an OS sandbox built from its `native-apps.json` entry (Seatbelt on macOS, Landlock and seccomp on Linux, not yet on Windows). A process app reaches its own agent over the **peer link**; the shell side is on `main`, but the Terminal is not granted an agent, so no process app uses it yet.
- **External clients**: Talk to Octos (opt-in) lets a web or terminal client use the system conversation with a limited external token: an allowlist of methods, no `peer/*` method, no app agent's session, no host-routed tools.
@@ -314,14 +314,14 @@ 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 the kernel service runs the packaged `octos-kernel` beside the shell, checked against this revision (`tools/kernel-artifact.py --host --stage` builds it); `OCTOS_APP_CORE_BIN` overrides it. |
| [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).
## AI services (octos)
-Each shell runs one [octos](https://github.com/octos-org/octos) agent kernel, started on first use: the APK's `liboctos.so` on Android, in process on OpenHarmony, the binary `OCTOS_APP_CORE_BIN` names on a desktop, none on iOS. The person chooses its models and types keys in the **AI providers** system app, on host sheets; keys stay in the platform's secret store and never reach an app. [`crates/ai-host`](crates/ai-host/README.md) is the shells' one entry point, and [`crates/app-peers`](crates/app-peers/README.md) gives each granted native app its own octos peer (private contexts, workspace and memory `app//acct-`), owned by the shell's system agent. Peer tool approvals use the shell router: developer mode, eligible standing rules or a person on the owning app/Shell confirmation sheet. The system agent cannot answer them.
+Each shell runs one [octos](https://github.com/octos-org/octos) agent kernel, started on first use: the APK's `liboctos.so` on Android, in process on OpenHarmony, on a desktop the packaged `octos-kernel` beside the shell (or the binary `OCTOS_APP_CORE_BIN` names), none on iOS. The person chooses its models and types keys in the **AI providers** system app, on host sheets; keys stay in the platform's secret store and never reach an app. [`crates/ai-host`](crates/ai-host/README.md) is the shells' one entry point, and [`crates/app-peers`](crates/app-peers/README.md) gives each granted native app its own octos peer (private contexts, workspace and memory `app//acct-`), owned by the shell's system agent. Peer tool approvals use the shell router: developer mode, eligible standing rules or a person on the owning app/Shell confirmation sheet. The system agent cannot answer them.
What works today: native modules (Rinx) use their peer; AppCard (opt-in) uses the kernel directly. Contained script apps, system or store, reach it through the `octos` host service in a shell that hosts a kernel: each app gets its own host-owned peer (`card.`), and its tool approvals go to the shell's approval sheets like every other app agent's ([#155](https://github.com/OctoSense-org/OctoSense/pull/155)). The `llm` service manages providers for `os.*` apps only. An app's own agent (`tools.json`, `AGENT.md`, skills, triggers, glance cards) is [ADR 0002](docs/adr/0002-event-driven-app-agents.md); host-service-backed `tools.json` tools reach their agents through the shell relay since [#160](https://github.com/OctoSense-org/OctoSense/pull/160). Tools marked `implemented_by: "app"` still lack a Card-runner executor; declaring a tool does not implement it.
@@ -367,6 +367,8 @@ cargo check --locked -p octosense --features mobile-apps
cargo check --locked -p octosense -p octosense-appcard --features mobile-apps,app-appcard
```
+The assistant needs the octos kernel beside the shell: `python3 tools/kernel-artifact.py --host --stage target/release` builds the pinned revision and stages it, once per octos pin; the desktop refuses a staged kernel of another revision and says so ([Build and run](desktop/README.md#build-and-run)). Without one the desktop runs without an assistant.
+
**Phone** (from `phone/`, which selects the phone's system apps; details in [phone/README.md](phone/README.md)):
```sh
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 5721b230..1c743b65 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -66,7 +66,7 @@ flowchart LR
- **Shell**(`crates/shell`,一个进程)承载窗口管理器、原生模块(App Hub、Rinx)、App Hub 的 Card runner(每个脚本应用都在自己的隔离环境中)、系统对话、审批路由、宿主工具中转,以及 [`crates/ai-host`](crates/ai-host/README.md);其中的 [app-peers 代理](crates/app-peers/README.md)就是内核的宿主连接。
-- **octos 内核**([`crates/kernel`](crates/kernel/README.zh-CN.md))首次使用时启动:桌面端(`OCTOS_APP_CORE_BIN`)和 Android(`liboctos.so`)上是通过 stdio 讲 OUP 的子进程,OpenHarmony 上是进程内的任务,iOS 上没有。它随 Shell 一起退出。
+- **octos 内核**([`crates/kernel`](crates/kernel/README.zh-CN.md))首次使用时启动:桌面端(Shell 旁随附的 `octos-kernel`,或 `OCTOS_APP_CORE_BIN`)和 Android(`liboctos.so`)上是通过 stdio 讲 OUP 的子进程,OpenHarmony 上是进程内的任务,iOS 上没有。它随 Shell 一起退出。
- **进程应用**:桌面端的 Terminal 作为独立进程运行,通过 Shell 的 hub 连接(画面和 AI bus),运行在按其 `native-apps.json` 条目构建的系统沙箱中(macOS 上是 Seatbelt,Linux 上是 Landlock 和 seccomp,Windows 上尚未实现)。进程应用通过 **peer link** 使用自己的 Agent;Shell 一侧已在 `main` 上,但 Terminal 没有被授予 Agent,所以目前还没有进程应用使用它。
- **外部客户端**:Talk to Octos(需手动开启)让网页或终端客户端以受限的外部 token 使用系统对话:只有一份方法白名单,不能调用任何 `peer/*` 方法,不能进入任何应用 Agent 的会话,也拿不到宿主路由的工具。
@@ -314,14 +314,14 @@ 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`,并核对其版本与此处固定的一致(由 `tools/kernel-artifact.py --host --stage` 构建);`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)。
## AI 服务(octos)
-每个 Shell 运行一个 [octos](https://github.com/octos-org/octos) Agent 内核,首次使用时启动:Android 上是 APK 中的 `liboctos.so`,OpenHarmony 上在进程内运行,桌面端运行 `OCTOS_APP_CORE_BIN` 指定的二进制,iOS 上没有。用户在系统应用 **AI providers** 中、在宿主面板上选择模型并输入密钥;密钥保存在平台的密钥存储中,永远不会到达应用。[`crates/ai-host`](crates/ai-host/README.md) 是两个 Shell 的统一入口,[`crates/app-peers`](crates/app-peers/README.md) 为每个获授权的原生应用分配自己的 octos peer(私有的上下文、工作区和记忆 `app//acct-`),归 Shell 的系统 Agent 所有。Peer 工具审批经 Shell 路由,由开发者模式、符合条件的常设规则,或用户在所属应用/Shell 确认面板上决定。系统 Agent 无法代答。
+每个 Shell 运行一个 [octos](https://github.com/octos-org/octos) Agent 内核,首次使用时启动:Android 上是 APK 中的 `liboctos.so`,OpenHarmony 上在进程内运行,桌面端运行 Shell 旁随附的 `octos-kernel`(或 `OCTOS_APP_CORE_BIN` 指定的二进制),iOS 上没有。用户在系统应用 **AI providers** 中、在宿主面板上选择模型并输入密钥;密钥保存在平台的密钥存储中,永远不会到达应用。[`crates/ai-host`](crates/ai-host/README.md) 是两个 Shell 的统一入口,[`crates/app-peers`](crates/app-peers/README.md) 为每个获授权的原生应用分配自己的 octos peer(私有的上下文、工作区和记忆 `app//acct-`),归 Shell 的系统 Agent 所有。Peer 工具审批经 Shell 路由,由开发者模式、符合条件的常设规则,或用户在所属应用/Shell 确认面板上决定。系统 Agent 无法代答。
目前可用的:原生模块(Rinx)使用自己的 peer;AppCard(需主动开启)直接使用内核。隔离运行的脚本应用,无论系统应用还是商店应用,在托管了内核的 Shell 中通过 `octos` 宿主服务使用助手:每个应用有自己的、由宿主拥有的 peer(`card.<应用 id>`),它的工具审批和其他应用 Agent 一样交给 Shell 的审批面板([#155](https://github.com/OctoSense-org/OctoSense/pull/155))。`llm` 服务只为 `os.*` 应用管理提供方。应用自己的 Agent(`tools.json`、`AGENT.md`、skills、触发器、glance 卡片)见 [ADR 0002](docs/adr/0002-event-driven-app-agents.md);自 [#160](https://github.com/OctoSense-org/OctoSense/pull/160) 起,应用 `tools.json` 中由宿主服务实现的工具已通过 Shell relay 提供给其 Agent。标为 `implemented_by: "app"` 的工具仍缺少 Card runner 执行器;声明不等于实现。
@@ -367,6 +367,8 @@ cargo check --locked -p octosense --features mobile-apps
cargo check --locked -p octosense -p octosense-appcard --features mobile-apps,app-appcard
```
+助手需要 Shell 旁的 octos 内核:`python3 tools/kernel-artifact.py --host --stage target/release` 构建固定版本并放到该处,每个 octos 固定版本做一次;桌面会拒绝版本不符的内核并说明原因([构建与运行](desktop/README.zh-CN.md#构建与运行))。没有内核时桌面在没有助手的情况下运行。
+
**手机端**(在 `phone/` 中运行,它会选择手机端的系统应用;详见 [phone/README.zh-CN.md](phone/README.zh-CN.md)):
```sh
diff --git a/crates/ai-host/README.md b/crates/ai-host/README.md
index 40570a6f..2b04a4cb 100644
--- a/crates/ai-host/README.md
+++ b/crates/ai-host/README.md
@@ -45,7 +45,8 @@ 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
+`octos-kernel` 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).
diff --git a/crates/ai-host/src/lib.rs b/crates/ai-host/src/lib.rs
index d4599603..46c342f5 100644
--- a/crates/ai-host/src/lib.rs
+++ b/crates/ai-host/src/lib.rs
@@ -93,7 +93,9 @@ pub enum KernelSource {
Bundled,
/// OpenHarmony: the canonical core linked in-process.
InProcess,
- /// Desktop: the binary `$OCTOS_APP_CORE_BIN` names; none without it (a
+ /// Desktop: the binary `$OCTOS_APP_CORE_BIN` names, else the packaged
+ /// `octos-kernel` beside the shell when its receipt names the pinned
+ /// octos revision (`octosense_kernel::launch`); none without either (a
/// developer's own `octos serve` is never touched).
Env,
/// Desktop or Android: this binary.
@@ -105,7 +107,8 @@ 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, `$OCTOS_APP_CORE_BIN` or the packaged kernel on a
+ /// desktop, none on iOS.
pub fn platform() -> Self {
if cfg!(target_os = "android") {
KernelSource::Bundled
diff --git a/crates/kernel/Cargo.toml b/crates/kernel/Cargo.toml
index 82dcf2f3..990493a5 100644
--- a/crates/kernel/Cargo.toml
+++ b/crates/kernel/Cargo.toml
@@ -38,6 +38,11 @@ octos-core = { workspace = true }
octos-cli = { workspace = true, features = ["api", "git", "ast"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
+# A desktop checks its packaged `octos-kernel` against the receipt's SHA-256
+# before running it (`launch::verify_packaged`).
+[target.'cfg(not(any(target_env = "ohos", target_os = "ios", target_os = "android")))'.dependencies]
+sha2 = "0.10"
+
# Passing the kept Talk to Octos listener to the kernel (`dup2` between fork
# and exec).
[target.'cfg(unix)'.dependencies]
diff --git a/crates/kernel/README.md b/crates/kernel/README.md
index 00c18a75..c5fb2981 100644
--- a/crates/kernel/README.md
+++ b/crates/kernel/README.md
@@ -42,8 +42,17 @@ How it starts, per platform (`src/launch.rs`):
with 8 MiB worker stacks (HAP native libraries may not exec).
- **Desktop**: ` serve --stdio --data-dir ` (plus
`--config /config.json` when that file exists) with
- `OCTOS_HOME=`; the program is the shell's `Options::program` or
- `$OCTOS_APP_CORE_BIN`. With neither there is no kernel: a developer's own
+ `OCTOS_HOME=`; the program is the shell's `Options::program`,
+ `$OCTOS_APP_CORE_BIN`, or the packaged `octos-kernel[.exe]` beside the
+ shell executable (in a macOS `.app`, `Contents/MacOS` or
+ `Contents/Resources`), in that order. The packaged kernel runs only when
+ its receipt `octos-kernel.json` (beside it, in the `.app`'s
+ `Contents/Resources`, or a Linux package's `usr/lib/octosense`) names the
+ octos revision this build pins (`build.rs` reads it from `Cargo.lock`) and
+ its SHA-256 matches; otherwise the connection fails with the reason.
+ `tools/kernel-artifact.py --host --stage ` builds and stages it;
+ `OCTOSENSE_KERNEL_ANY_REVISION=1` accepts another revision while
+ developing. No `PATH` search, no working directory, and a developer's own
`octos serve` is never touched.
- **iOS**: no kernel.
- **Talk to Octos on** (desktop and Android): the same command with
diff --git a/crates/kernel/README.zh-CN.md b/crates/kernel/README.zh-CN.md
index 710405e0..8843052b 100644
--- a/crates/kernel/README.zh-CN.md
+++ b/crates/kernel/README.zh-CN.md
@@ -38,8 +38,14 @@ Shell(`phone/` 中的 Home、`desktop/` 中的桌面)拥有它;**AI provid
运行在本 crate 的运行时上,工作线程栈 8 MiB(HAP 原生库不能 exec)。
- **桌面**:` serve --stdio --data-dir `(`/config.json`
存在时再加 `--config /config.json`),`OCTOS_HOME=`;程序为 Shell 的
- `Options::program` 或 `$OCTOS_APP_CORE_BIN`。两者都没有时就没有内核:绝不会动开发者
- 自己的 `octos serve`。
+ `Options::program`、`$OCTOS_APP_CORE_BIN`,或 Shell 可执行文件旁随附的
+ `octos-kernel[.exe]`(macOS `.app` 中为 `Contents/MacOS` 或 `Contents/Resources`),
+ 依次选用。随附内核只有在其收据 `octos-kernel.json`(在它旁边、`.app` 的
+ `Contents/Resources` 或 Linux 包的 `usr/lib/octosense` 中)记录的版本与本次构建固定的
+ octos 版本一致(`build.rs` 从 `Cargo.lock` 读取),且 SHA-256 相符时才运行;否则连接
+ 失败并给出原因。`tools/kernel-artifact.py --host --stage <目录>` 负责构建和放置;开发时
+ `OCTOSENSE_KERNEL_ANY_REVISION=1` 可接受其他版本。不在 `PATH` 或工作目录中查找,绝不会
+ 动开发者自己的 `octos serve`。
- **iOS**:没有内核。
- **开启 Talk to Octos 时**(桌面和 Android):同一命令,以 `--host 127.0.0.1 --host-managed`
代替 `--stdio`,并把本进程保留的监听套接字作为描述符 3 传入(`--listen-fd 3`,Unix)。
diff --git a/crates/kernel/build.rs b/crates/kernel/build.rs
new file mode 100644
index 00000000..2d3c79ff
--- /dev/null
+++ b/crates/kernel/build.rs
@@ -0,0 +1,58 @@
+//! The octos revision this build pins, for the desktop launch check
+//! (`launch::verify_packaged`): a packaged `octos-kernel` runs only when its
+//! receipt names this revision.
+//!
+//! Read from the workspace's Cargo.lock (the `octos-cli` the Android and
+//! desktop kernel artifacts are built from, as tools/kernel-artifact.py reads
+//! it; else `octos-core`, which this crate links). `OCTOSENSE_OCTOS_REVISION`
+//! at build time overrides it. Empty when neither is found: the desktop then
+//! refuses a packaged kernel it cannot check (an explicit program still runs).
+
+use std::path::Path;
+
+fn main() {
+ println!("cargo:rerun-if-env-changed=OCTOSENSE_OCTOS_REVISION");
+ let lock = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../Cargo.lock");
+ println!("cargo:rerun-if-changed={}", lock.display());
+ let revision = std::env::var("OCTOSENSE_OCTOS_REVISION")
+ .ok()
+ .filter(|r| is_revision(r))
+ .or_else(|| std::fs::read_to_string(&lock).ok().and_then(|text| pinned(&text)))
+ .unwrap_or_default();
+ println!("cargo:rustc-env=OCTOSENSE_PINNED_OCTOS_REVISION={revision}");
+}
+
+fn is_revision(text: &str) -> bool {
+ text.len() == 40 && text.bytes().all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase())
+}
+
+/// The one octos-org/octos revision of `octos-cli` (else `octos-core`) in a
+/// Cargo.lock; none when absent or ambiguous.
+fn pinned(lock: &str) -> Option {
+ const SOURCE: &str = "git+https://github.com/octos-org/octos.git?rev=";
+ for crate_name in ["octos-cli", "octos-core"] {
+ let mut found = Vec::new();
+ for block in lock.split("[[package]]") {
+ let field = |key: &str| {
+ block.lines().find_map(|line| {
+ let rest = line.strip_prefix(key)?.trim_start().strip_prefix('=')?.trim();
+ Some(rest.trim_matches('"').to_owned())
+ })
+ };
+ if field("name").as_deref() != Some(crate_name) {
+ continue;
+ }
+ if let Some(rev) = field("source").and_then(|s| s.strip_prefix(SOURCE).map(|r| r.split('#').next().unwrap_or("").to_owned())) {
+ if is_revision(&rev) && !found.contains(&rev) {
+ found.push(rev);
+ }
+ }
+ }
+ match found.len() {
+ 0 => continue,
+ 1 => return found.pop(),
+ _ => return None,
+ }
+ }
+ None
+}
diff --git a/crates/kernel/src/launch.rs b/crates/kernel/src/launch.rs
index bd3717f3..ebe15a96 100644
--- a/crates/kernel/src/launch.rs
+++ b/crates/kernel/src/launch.rs
@@ -10,9 +10,15 @@
//! (`octos_cli::embedded::serve_io`); HAP native libraries cannot exec.
//! - **Desktop**: ` serve --stdio --data-dir --config
//! /config.json` with `OCTOS_HOME=`, 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 a
+//! macOS `.app`, `Contents/MacOS` or `Contents/Resources`), in that order.
+//! The packaged kernel runs only when its receipt (`octos-kernel.json`,
+//! written by `tools/kernel-artifact.py --stage`) names the octos revision
+//! this build pins (checked when resolved, `verify_packaged`) and the
+//! kernel's SHA-256 (checked by [`prepare`] before each start, off the
+//! caller's thread). No PATH search and no attaching to another running
+//! kernel (a developer's own `octos serve` is never touched).
//! - **iOS**: no kernel (an app cannot exec a child).
//!
//! With Talk to Octos on (desktop and Android), `--stdio` becomes
@@ -122,20 +128,250 @@ pub(crate) fn resolve(inputs: &Inputs) -> Result {
}
#[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(PROGRAM_ENV).filter(|v| !v.is_empty()).map(PathBuf::from);
+ // Through symlinks (a launcher linked into a bin dir): the kernel
+ // travels with the real executable.
+ let executable = std::env::current_exe().ok().map(|p| p.canonicalize().unwrap_or(p));
+ let any_revision = std::env::var_os(ANY_REVISION_ENV).is_some_and(|v| v == "1");
+ let program = desktop_program(inputs.program, env.as_deref(), executable.as_deref(), PINNED_REVISION, any_revision)?;
Ok(desktop_stdio(program, core_dir, inputs.env))
}
}
+/// The desktop's explicit kernel: wins over the packaged one, unchecked (the
+/// person chose it).
+#[cfg(not(any(target_env = "ohos", target_os = "ios", target_os = "android")))]
+pub const PROGRAM_ENV: &str = "OCTOS_APP_CORE_BIN";
+/// Development only: `=1` runs a packaged kernel whose receipt names another
+/// octos revision than this build pins (a missing receipt or a SHA-256
+/// mismatch is still refused).
+#[cfg(not(any(target_env = "ohos", target_os = "ios", target_os = "android")))]
+pub const ANY_REVISION_ENV: &str = "OCTOSENSE_KERNEL_ANY_REVISION";
+/// The packaged kernel's file name, beside the shell executable.
+#[cfg(not(any(target_env = "ohos", target_os = "ios", target_os = "android")))]
+pub const PACKAGED_KERNEL: &str = if cfg!(windows) { "octos-kernel.exe" } else { "octos-kernel" };
+/// Its receipt: `{"source", "revision", "version", "sha256"}`.
+#[cfg(not(any(target_env = "ohos", target_os = "ios", target_os = "android")))]
+pub const PACKAGED_RECEIPT: &str = "octos-kernel.json";
+/// The octos revision this build pins (the workspace's Cargo.lock, read by
+/// build.rs); empty when the build could not tell.
+#[cfg(not(any(target_env = "ohos", target_os = "ios", target_os = "android")))]
+pub const PINNED_REVISION: &str = env!("OCTOSENSE_PINNED_OCTOS_REVISION");
+
+/// The kernel program on a desktop: `explicit` (`Options::program`), else
+/// `env` (`$OCTOS_APP_CORE_BIN`), else the packaged kernel that travels with
+/// `executable`, wherever it was installed or moved (never the working
+/// directory). An override is authoritative: a broken one is an error, never
+/// a fallback to another kernel. The packaged one must pass
+/// [`verify_packaged`].
+#[cfg(not(any(target_env = "ohos", target_os = "ios", target_os = "android")))]
+fn desktop_program(
+ explicit: Option<&Path>,
+ env: Option<&Path>,
+ executable: Option<&Path>,
+ pinned: &str,
+ any_revision: bool,
+) -> Result {
+ if let Some(program) = explicit.or(env) {
+ check_executable(program)?;
+ return Ok(program.to_owned());
+ }
+ let Some(dir) = executable.and_then(Path::parent) else {
+ return Err(Unavailable::NoKernel(format!("no packaged kernel and no {PROGRAM_ENV} override")));
+ };
+ let Some(program) = packaged_candidates(dir).into_iter().find(|p| p.is_file()) else {
+ return Err(Unavailable::NoKernel(format!(
+ "no packaged {PACKAGED_KERNEL} beside {} and no {PROGRAM_ENV} override; \
+ stage one with `python3 tools/kernel-artifact.py --host --stage `",
+ dir.display()
+ )));
+ };
+ check_executable(&program)?;
+ let receipt = receipt_candidates(dir, &program).into_iter().find(|p| p.is_file());
+ let sha256 = verify_packaged(&program, receipt.as_deref(), pinned, any_revision)?;
+ // Hashing a 100 MB kernel takes most of a second: not here (resolving
+ // also answers status queries on the UI thread) but in `prepare`, right
+ // before the start.
+ expect_sha256(&program, sha256);
+ Ok(program)
+}
+
+/// Where a packaged kernel may sit, relative to the shell executable's dir:
+/// beside it (a source build's `target/`, the Windows install dir,
+/// Linux `usr/bin`, a `.app`'s `Contents/MacOS`), or a `.app`'s
+/// `Contents/Resources`.
+#[cfg(not(any(target_env = "ohos", target_os = "ios", target_os = "android")))]
+fn packaged_candidates(dir: &Path) -> Vec {
+ let mut found = vec![dir.join(PACKAGED_KERNEL)];
+ if let Some(resources) = bundle_resources(dir) {
+ found.push(resources.join(PACKAGED_KERNEL));
+ }
+ found
+}
+
+/// Where its receipt may sit: beside the kernel, a `.app`'s
+/// `Contents/Resources` (nothing but code belongs in `Contents/MacOS`), or a
+/// Linux package's `usr/lib/octosense` (not `usr/bin`).
+#[cfg(not(any(target_env = "ohos", target_os = "ios", target_os = "android")))]
+fn receipt_candidates(dir: &Path, program: &Path) -> Vec {
+ let mut found = vec![program.with_file_name(PACKAGED_RECEIPT)];
+ if let Some(resources) = bundle_resources(dir) {
+ found.push(resources.join(PACKAGED_RECEIPT));
+ }
+ if dir.file_name().is_some_and(|n| n == "bin") {
+ if let Some(prefix) = dir.parent() {
+ found.push(prefix.join("lib/octosense").join(PACKAGED_RECEIPT));
+ }
+ }
+ found
+}
+
+/// `.app/Contents/Resources` when `dir` is `.app/Contents/MacOS`.
+#[cfg(not(any(target_env = "ohos", target_os = "ios", target_os = "android")))]
+fn bundle_resources(dir: &Path) -> Option {
+ let contents = dir.parent()?;
+ let is_bundle = dir.file_name()? == "MacOS"
+ && contents.file_name()? == "Contents"
+ && contents.parent()?.extension().is_some_and(|e| e == "app");
+ is_bundle.then(|| contents.join("Resources"))
+}
+
+#[cfg(not(any(target_env = "ohos", target_os = "ios", target_os = "android")))]
+fn check_executable(program: &Path) -> Result<(), Unavailable> {
+ if !program.is_file() {
+ return Err(Unavailable::NoKernel(format!("{} is not a file", 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(())
+}
+
+/// A packaged kernel runs only when it is the one this build pins: its
+/// receipt names `pinned` (unless `any_revision`, a development override)
+/// and records a SHA-256, which [`prepare`] checks against the file before
+/// every start ([`check_sha256`]). So an old `target/release/octos-kernel`
+/// left from an earlier pin, a kernel without a receipt or a binary swapped
+/// in later is refused with a reason the person sees, never run silently.
+/// Returns the recorded SHA-256. Reads only the small receipt.
+#[cfg(not(any(target_env = "ohos", target_os = "ios", target_os = "android")))]
+pub(crate) fn verify_packaged(program: &Path, receipt: Option<&Path>, pinned: &str, any_revision: bool) -> Result {
+ let refuse = |why: String| Err(Unavailable::NoKernel(refusal(program, &why)));
+ let Some(receipt) = receipt else {
+ return refuse(format!("it has no receipt ({PACKAGED_RECEIPT}) saying which octos revision it is"));
+ };
+ let text = match std::fs::read_to_string(receipt) {
+ Ok(text) => text,
+ Err(e) => return refuse(format!("its receipt {} cannot be read ({e})", receipt.display())),
+ };
+ let Ok(record) = serde_json::from_str::(&text) else {
+ return refuse(format!("its receipt {} is not JSON", receipt.display()));
+ };
+ let revision = record["revision"].as_str().unwrap_or("");
+ let sha256 = record["sha256"].as_str().unwrap_or("").to_ascii_lowercase();
+ if revision.is_empty() || sha256.len() != 64 || !sha256.bytes().all(|b| b.is_ascii_hexdigit()) {
+ return refuse(format!("its receipt {} names no revision and SHA-256", receipt.display()));
+ }
+ if revision != pinned {
+ if pinned.is_empty() {
+ if !any_revision {
+ return refuse("this build does not know which octos revision it pins".into());
+ }
+ } else if any_revision {
+ log::warn!(
+ "octos-core: {ANY_REVISION_ENV}=1: running the packaged kernel at octos {} although this build pins {}",
+ short(revision),
+ short(pinned)
+ );
+ } else {
+ return refuse(format!(
+ "it is octos {} but this build pins {} (set {ANY_REVISION_ENV}=1 to run it anyway while developing)",
+ short(revision),
+ short(pinned)
+ ));
+ }
+ }
+ Ok(sha256)
+}
+
+#[cfg(not(any(target_env = "ohos", target_os = "ios", target_os = "android")))]
+fn refusal(program: &Path, why: &str) -> String {
+ format!(
+ "refusing the packaged kernel {}: {why}. Rebuild it with `python3 tools/kernel-artifact.py --host --stage {}` \
+ or name a kernel with {PROGRAM_ENV}",
+ program.display(),
+ program.parent().map(|p| p.display().to_string()).unwrap_or_default()
+ )
+}
+
+/// Packaged kernels resolved, and the SHA-256 their receipts record.
+#[cfg(not(any(target_env = "ohos", target_os = "ios", target_os = "android")))]
+type Expected = std::collections::HashMap;
+#[cfg(not(any(target_env = "ohos", target_os = "ios", target_os = "android")))]
+static EXPECTED: std::sync::Mutex