From bc55b2a634e753e5b8017fdf631667510d1f59a7 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:41:25 -0700 Subject: [PATCH] docs: architecture overview (processes, agents, protocols, approvals) [skip ci] docs/architecture.md and .zh-CN.md describe how OctoSense fits together on main: processes per platform (the shell, the octos kernel child or in-process core, in-process and process native apps, script apps in the Card runner), agents as kernel sessions, OUP over stdio and host-managed WebSocket, the peer blackboard and the planned peer/input path (octos#2567), tools and grants, the approval router, storage and secrets, trust boundaries, and a worked Calendar/Mail example with the status of every step. It ends with the places where the code and the ADRs disagree. The READMEs get a short "How it fits together" section; the ADR index and ai-services link to the new page. Docs only. Co-Authored-By: Claude Opus 5.5 (1M context) --- README.md | 20 +- README.zh-CN.md | 20 +- docs/adr/README.md | 2 + docs/adr/README.zh-CN.md | 2 + docs/ai-services.md | 2 + docs/ai-services.zh-CN.md | 2 + docs/architecture.md | 456 +++++++++++++++++++++++++++++++++++++ docs/architecture.zh-CN.md | 456 +++++++++++++++++++++++++++++++++++++ 8 files changed, 958 insertions(+), 2 deletions(-) create mode 100644 docs/architecture.md create mode 100644 docs/architecture.zh-CN.md diff --git a/README.md b/README.md index d7b936d0..dbdd5a97 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,24 @@ It was OctoSense-Desktop; OctoSense-ROM (retired; merged into this repository) a > **Building an OctoSense app?** You do not need this repository to build, check or publish one. Start at the [OctoSense-org profile](https://github.com/OctoSense-org)'s reading list: [OctoScript-App-Design-Flow](https://github.com/OctoSense-org/OctoScript-App-Design-Flow) (`AGENTS.md`, then `docs/QUICKSTART.md`) and [OctoSense-App-Hub](https://github.com/OctoSense-org/OctoSense-App-Hub). The system apps in [`apps/`](apps/README.md) are complete examples of the same app shape (`apps//bundle/`). Build the desktop shell from here only to see your app in a shell before it is published ([PUBLISHING §4](https://github.com/OctoSense-org/OctoScript-App-Design-Flow/blob/main/docs/PUBLISHING.md#4-rehearse-the-store-path-locally)). +## How it fits together + +``` + person ──> OctoSense shell (one process) ──── OUP (stdio) ────> octos kernel (one per shell) + ├─ native modules: App Hub, Rinx ├─ system agent session + ├─ Card runner: script apps in isolates └─ one app agent (peer) per (app, account) + ├─ approval router, host sheets, storage + └─ hub ──> process apps (desktop: Terminal) +``` + +- **One shell process** hosts the window manager, the native modules (App Hub, Rinx) and App Hub's Card runner, where every script app runs in its own isolate. On the desktop the Terminal runs as its own process, attached over the shell's hub. +- **One octos kernel per shell**, started on first use: a child process on the desktop and Android, in process on OpenHarmony, none on iOS. The shell is its host connection and holds the host token; apps never talk to the kernel. +- **Agents are sessions in that kernel**: the system agent, and one app agent (an octos peer) per app and account, each with its own workspace, memory and transcript. The system agent briefs app agents and reads their results on the peers' blackboard. +- **Apps reach their agent through the shell**: an injected service for native modules, `host.request("octos.*")` for script apps. Talk to Octos (opt-in) lets a web or terminal client use the system conversation with a limited token, never the app agents. +- **The person approves**: outward and destructive tool calls go through the shell's approval router, live on a sheet or by a standing rule the person set. + +Processes, agents, protocols, tools, approvals, storage and trust boundaries, with what is on `main` and what is planned: [docs/architecture.md](docs/architecture.md). + ## Layout | Path | What it is | @@ -53,7 +71,7 @@ Each shell runs one [octos](https://github.com/octos-org/octos) agent kernel, st 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 tool approvals are declined until the Card runner has an approval sheet. 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), Proposed, with its first pieces merged or in review. -The architecture, the trust model, what each kind of app can use, the plan with its status, and how to run and test it locally: [docs/ai-services.md](docs/ai-services.md). For app developers: OctoScript-App-Design-Flow's [AI-SERVICES](https://github.com/OctoSense-org/OctoScript-App-Design-Flow/blob/main/docs/AI-SERVICES.md). +The architecture, the trust model, what each kind of app can use, the plan with its status, and how to run and test it locally: [docs/ai-services.md](docs/ai-services.md). How it fits into the whole system: [docs/architecture.md](docs/architecture.md). For app developers: OctoScript-App-Design-Flow's [AI-SERVICES](https://github.com/OctoSense-org/OctoScript-App-Design-Flow/blob/main/docs/AI-SERVICES.md). ## Set up diff --git a/README.zh-CN.md b/README.zh-CN.md index 618496a9..e9019e2b 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -14,6 +14,24 @@ > **要开发 OctoSense 应用?** 开发、检查或发布应用都不需要本仓库。请从 [OctoSense-org 主页](https://github.com/OctoSense-org)的阅读列表开始:[OctoScript-App-Design-Flow](https://github.com/OctoSense-org/OctoScript-App-Design-Flow)(先读 `AGENTS.md`,再读 `docs/QUICKSTART.md`)和 [OctoSense-App-Hub](https://github.com/OctoSense-org/OctoSense-App-Hub)。[`apps/`](apps/README.zh-CN.md) 中的系统应用是同样应用结构的完整示例(`apps//bundle/`)。只有想在发布前先在 Shell 里看到自己的应用时,才需要从这里构建桌面端 Shell([PUBLISHING §4](https://github.com/OctoSense-org/OctoScript-App-Design-Flow/blob/main/docs/PUBLISHING.md#4-rehearse-the-store-path-locally))。 +## 整体如何运作 + +``` + 用户 ──> OctoSense Shell(一个进程)──── OUP(stdio)────> octos 内核(每个 Shell 一个) + ├─ 原生模块:App Hub、Rinx ├─ 系统 Agent 会话 + ├─ Card runner:脚本应用各在隔离环境中 └─ 每个(应用,账号)一个应用 Agent(peer) + ├─ 审批路由、宿主面板、存储 + └─ hub ──> 进程应用(桌面端:Terminal) +``` + +- **一个 Shell 进程**承载窗口管理器、原生模块(App Hub、Rinx)和 App Hub 的 Card runner,每个脚本应用都在自己的隔离环境中运行。桌面端的 Terminal 作为独立进程运行,通过 Shell 的 hub 连接。 +- **每个 Shell 一个 octos 内核**,首次使用时启动:桌面端和 Android 上是子进程,OpenHarmony 上在进程内,iOS 上没有。Shell 是它的宿主连接并持有宿主 token;应用从不直接与内核通信。 +- **Agent 是这个内核中的会话**:系统 Agent,以及每个应用、每个账号一个应用 Agent(octos peer),各自有独立的工作区、记忆和对话记录。系统 Agent 向应用 Agent 下达任务,并在 peer 黑板上读取它们的结果。 +- **应用通过 Shell 使用自己的 Agent**:原生模块用注入的服务,脚本应用用 `host.request("octos.*")`。Talk to Octos(需手动开启)让网页或终端客户端以受限 token 使用系统对话,但永远接触不到应用 Agent。 +- **由用户批准**:对外和破坏性的工具调用都经过 Shell 的审批路由,在面板上实时批准,或由用户设定的常设规则批准。 + +进程、Agent、协议、工具、审批、存储和信任边界,以及哪些已在 `main` 上、哪些还在规划中:[docs/architecture.zh-CN.md](docs/architecture.zh-CN.md)。 + ## 目录结构 | 路径 | 内容 | @@ -53,7 +71,7 @@ Shell 只有一份,位于 `crates/shell`([ADR 0001(英文)](docs/adr/000 目前可用的:原生模块(Rinx)使用自己的 peer;AppCard(需主动开启)直接使用内核。隔离运行的脚本应用,无论系统应用还是商店应用,在托管了内核的 Shell 中通过 `octos` 宿主服务使用助手:每个应用有自己的、由宿主拥有的 peer(`card.<应用 id>`),在 Card runner 有审批面板之前工具审批一律被拒绝。`llm` 服务只为 `os.*` 应用管理提供方。应用自己的 Agent(`tools.json`、`AGENT.md`、skills、触发器、glance 卡片)见 [ADR 0002](docs/adr/0002-event-driven-app-agents.md)(Proposed),其中最早的几部分已合入或正在评审。 -架构、信任模型、各类应用能用什么、规划及其状态,以及如何在本地运行和测试:[docs/ai-services.zh-CN.md](docs/ai-services.zh-CN.md)。面向应用开发者:OctoScript-App-Design-Flow 的 [AI-SERVICES](https://github.com/OctoSense-org/OctoScript-App-Design-Flow/blob/main/docs/AI-SERVICES.zh-CN.md)。 +架构、信任模型、各类应用能用什么、规划及其状态,以及如何在本地运行和测试:[docs/ai-services.zh-CN.md](docs/ai-services.zh-CN.md)。它在整个系统中的位置:[docs/architecture.zh-CN.md](docs/architecture.zh-CN.md)。面向应用开发者:OctoScript-App-Design-Flow 的 [AI-SERVICES](https://github.com/OctoSense-org/OctoScript-App-Design-Flow/blob/main/docs/AI-SERVICES.zh-CN.md)。 ## 环境准备 diff --git a/docs/adr/README.md b/docs/adr/README.md index 7ba2864f..f097a83b 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -4,6 +4,8 @@ English | [简体中文](README.zh-CN.md) Decisions for the OctoSense repository: the shell, its services, the system apps and the desktop, phone and ROM packagings. New ADRs go here, numbered after the last one in this table. +How these decisions fit together in the code on `main`, and which parts are still planned: [OctoSense architecture](../architecture.md). + | ADR | Title | Status | | --- | --- | --- | | [0001](0001-one-octosense-repository.md) | One OctoSense repository for the shell, its services, the system apps and both packagings | Accepted | diff --git a/docs/adr/README.zh-CN.md b/docs/adr/README.zh-CN.md index 4d4b03f2..f90a425c 100644 --- a/docs/adr/README.zh-CN.md +++ b/docs/adr/README.zh-CN.md @@ -4,6 +4,8 @@ OctoSense 仓库的决策:Shell、Shell 服务、系统应用,以及桌面、手机和 ROM 三种打包形态。新的 ADR 放在这里,编号接在下表最后一条之后。 +这些决策在 `main` 的代码中如何组合在一起、哪些部分仍在规划中:[OctoSense 架构](../architecture.zh-CN.md)。 + | ADR | 标题 | 状态 | | --- | --- | --- | | [0001](0001-one-octosense-repository.md)(英文) | 用一个 OctoSense 仓库承载 Shell、Shell 服务、系统应用和两种打包形态 | 已接受 | diff --git a/docs/ai-services.md b/docs/ai-services.md index 9c727bc2..6e7c54cf 100644 --- a/docs/ai-services.md +++ b/docs/ai-services.md @@ -6,6 +6,8 @@ How the assistant is wired into OctoSense: the [octos](https://github.com/octos- This page is about the assistant *inside* OctoSense. Building an app needs no AI service and no particular coding agent: the app harness, [OctoScript-App-Design-Flow](https://github.com/OctoSense-org/OctoScript-App-Design-Flow), works with any agent or none. Its [AI-SERVICES](https://github.com/OctoSense-org/OctoScript-App-Design-Flow/blob/main/docs/AI-SERVICES.md) page is the app developer's short version of this one. +For the whole system around it (processes per platform, agents, protocols, tools and grants, approvals, storage and trust boundaries), see [OctoSense architecture](architecture.md). + ## Contents - [At a glance](#at-a-glance) diff --git a/docs/ai-services.zh-CN.md b/docs/ai-services.zh-CN.md index 9f715ade..97936f2f 100644 --- a/docs/ai-services.zh-CN.md +++ b/docs/ai-services.zh-CN.md @@ -6,6 +6,8 @@ 本文讨论的是 OctoSense *内部*的助手。开发应用不需要任何 AI 服务,也不需要特定的编程 Agent:应用开发工具集 [OctoScript-App-Design-Flow](https://github.com/OctoSense-org/OctoScript-App-Design-Flow) 适用于任何 Agent,也可以不用 Agent。它的 [AI-SERVICES](https://github.com/OctoSense-org/OctoScript-App-Design-Flow/blob/main/docs/AI-SERVICES.zh-CN.md) 页面是本文面向应用开发者的简短版本。 +它所处的整个系统(各平台的进程、Agent、协议、工具与授权、审批、存储和信任边界)见 [OctoSense 架构](architecture.zh-CN.md)。 + ## 目录 - [概览](#概览) diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 00000000..b4d0a30c --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,456 @@ +# OctoSense architecture + +English | [简体中文](architecture.zh-CN.md) + +How OctoSense fits together: which processes run on each platform, where the agents live, how the pieces talk, how tools are granted and approved, where data and secrets are kept, and where the trust boundaries are. It describes `main` on 2026-09-28 (OctoSense `ff40e9c`, which pins octos `5e7577f0`), read in the code. + +Every statement carries its status: + +- **On main**: merged, read in this repository's code (a path is given). +- **In progress**: an open pull request, linked. +- **Planned**: decided in an ADR (linked, with its plan step) and not built yet. + +The decisions behind it are [ADR 0001](adr/0001-one-octosense-repository.md) (one repository), [ADR 0002](adr/0002-event-driven-app-agents.md) (app agents, Proposed, as amended by 0004), [ADR 0003](adr/0003-shared-octos-client-access.md) (Talk to Octos) and [ADR 0004](adr/0004-native-apps-hosting-and-peers.md) (native apps, app agents, cross-app work and approvals, Accepted). The assistant's services, what each kind of app can call today and how to run them locally are in [ai-services.md](ai-services.md); this page does not repeat them. + +## Contents + +- [The big picture](#the-big-picture) +- [1. Processes per platform](#1-processes-per-platform) +- [2. Agents](#2-agents) +- [3. Communication](#3-communication) +- [4. Tools and grants](#4-tools-and-grants) +- [5. Approvals](#5-approvals) +- [6. Storage and secrets](#6-storage-and-secrets) +- [7. Trust boundaries and isolation](#7-trust-boundaries-and-isolation) +- [8. Worked example: emailing a meeting invite](#8-worked-example-emailing-a-meeting-invite) +- [Where the code and the ADRs disagree](#where-the-code-and-the-adrs-disagree) +- [Source map](#source-map) + +## The big picture + +```mermaid +flowchart LR + person(["The person"]) + subgraph shellp["OctoSense shell process (desktop, or Home on a phone)"] + wm["Window manager, launcher, sheets
crates/shell"] + mods["In-process native modules
App Hub, Rinx, (AppCard)"] + runner["App Hub Card runner
script apps in isolates"] + router["Approval router
crates/shell/src/approvals"] + aihost["ai-host + app-peers broker
host connection to the kernel"] + bus["AI services bus
crates/shell/src/ai_bus.rs"] + end + term["Process apps (desktop)
Terminal; opt-in Sheets, Reference"] + subgraph kern["octos kernel (one per shell)"] + sys["System agent session
(profile _main)"] + peers["App peers
one per (app, account)"] + end + ext["Talk to Octos clients
web, terminal (opt-in)"] + person --> wm + wm --- mods + wm --- runner + term <-->|"hub (loopback WebSocket)"| wm + mods -- "injected OctosAppService" --> aihost + runner -- "host.request(octos.*)" --> aihost + aihost <-->|"OUP over stdio
(or host-token WebSocket)"| kern + sys -- "peer_send_input / blackboard" --> peers + ext -. "external token, allowlist" .-> sys + term -- "typed tools" --> bus + mods -- "typed tools" --> bus + bus --> router +``` + +``` ++--------------------------- OctoSense shell process ----------------------------+ +| window manager / launcher / sheets approval router AI services bus | +| in-process modules: App Hub (+ Card runner with script apps), Rinx, (AppCard) | +| ai-host + app-peers broker == host connection ==+ | ++---------------------------------------------------|-----------------------------+ + ^ hub: loopback WebSocket | OUP: stdio (default) or + | | WebSocket with the host token ++-------+---------+ +--------------v--------------------------+ +| process apps | | octos kernel (child; in process on OHOS)| +| Terminal, ... | | system agent session | ++-----------------+ | app peers: one per (app, account) | + | tools run as short child processes | + Talk to Octos clients ---------> +-----------------------------------------+ + (external token, allowlist, opt-in) +``` + +## 1. Processes per platform + +### The shell + +One Makepad process per device: the desktop (`desktop/`, package `octosense`) or Home on a phone (`phone/`, package `octosense-home`), both linking the one shell crate `crates/shell` ([ADR 0001](adr/0001-one-octosense-repository.md)). The shell owns the window manager, the launcher, the host sheets, the approval router, app storage and the kernel's host connection. + +### The octos kernel + +Each shell runs at most one [octos](https://github.com/octos-org/octos) kernel, a shell service in [`crates/kernel`](../crates/kernel/README.md) (package `octosense-kernel`), reached through [`crates/ai-host`](../crates/ai-host/README.md). **On main.** How it runs (`crates/kernel/src/launch.rs`, `launch::resolve`): + +| Platform | Kernel | How it is started | +| --- | --- | --- | +| Desktop (macOS; Windows and Linux untested) | a child process: the `octos` binary named by `OCTOS_APP_CORE_BIN` (or the embedder's `Options::program`) | `serve --stdio --data-dir `; without a binary there is no kernel (no `PATH` lookup). Shipping the binary with the desktop is in progress ([#85](https://github.com/OctoSense-org/OctoSense/pull/85)). | +| Android (Home) | a child process: the APK's bundled `liboctos.so`, built by [`tools/kernel-artifact.py`](../tools/kernel-artifact.py) at the pinned octos revision | `serve --stdio`, found next to `libmakepad.so` in the app's native library directory | +| OpenHarmony | in process: `octos_cli::embedded::serve_io` as a task over an in-memory duplex (a HAP may not exec) | same protocol, no child | +| iOS | none | the providers are saved; no app gets an assistant | + +Its lifecycle (`crates/kernel/src/lib.rs`, `kernel.rs`): + +- **Lazy start, shared.** Nothing runs until the first consumer calls `connect()`; later consumers attach to the same generation. A new generation waits until the previous one has exited, so octos's single-writer lock on its data directory is released first. +- **Restart on provider change.** The `llm` host service rewrites the profile and calls `restart()`: every connection ends with `CloseReason::Restarted` and the consumers reconnect to a fresh kernel. +- **Idle stop.** With Talk to Octos off, the kernel stops when the last connection closes. With it on, it keeps running until the person turns it off or the shell exits. +- **Exit with the shell.** The shell holds the child's stdin for its whole life; when the shell exits, crashes or is killed, the kernel reads EOF and stops. In `--host-managed` mode octos also asks for SIGTERM on parent death on Linux and Android (`bind_to_parent()` in octos `crates/octos-cli/src/api/host_managed.rs`); the child is also spawned `kill_on_drop`. +- **A crash** ends every connection with `CloseReason::Exited` (the last stderr lines); nothing restarts it by itself, and the next `connect()` starts a new generation. See [what a kernel crash means](#what-a-kernel-crash-means). + +### Native apps: in process or their own process + +Native apps are first-party Rust crates declared only in [`native-apps.json`](../native-apps.json); `tools/native_apps.py` generates `crates/shell/src/native_apps.rs` and the Cargo blocks from it and checks them in CI (ADR 0004 §1, plan step 1). **On main.** The manifest today: + +| App | Hosting on macOS / Windows | Linux | Android, iOS, OpenHarmony | Desktop / phone | Assistant | +| --- | --- | --- | --- | --- | --- | +| App Hub (`apphub`: the store and the Card runner) | in process | in process | in process | default / default | – | +| Rinx | in process | in process | in process | default / default | granted the four `octos.*` services | +| Terminal | **own process** | own process with a Vulkan build and a Wayland session, else in process | in process | default / off | – (its `run` tool is `confirm: host`, `auto_approvable: false`) | +| Sheets, Reference | own process | own process | in process | opt-in / `mobile-apps` | – | +| AppCard | in process | in process | in process | opt-in / opt-in | its own kernel connection | + +So among the apps a shell ships by default, **only the Terminal is a process app**, and only on the desktop. App Hub stays in process because it hosts the Card runner every script app runs in; Rinx stays in process until the peer link and the process sandboxes exist (ADR 0004 §2). + +How the shell decides at run time (`crates/shell/src/apps.rs`, `AppRegistry::hosting`): if the target cannot run processes (`host::processes_available()` is false on wasm and native mobile), every app is a module; App Hub and Settings are always modules; otherwise a native app follows its manifest entry, and runs as a process only when a process form exists (a checkout to `cargo run` from, or a sibling binary). Release packages do not ship process apps' binaries yet ([#94](https://github.com/OctoSense-org/OctoSense/pull/94), in progress), so there they fall back to in-process. A per-app override (`~/.makepad/wm/apps.splash`, `--module `) can switch a module to a process. + +**Process hosting** is Makepad's window-manager hosting (`crates/shell/src/clients.rs`, `hub.rs`): + +- The shell starts the app as a child with `--stdin-loop` (`cargo run … -- --stdin-loop` in a checkout, else the sibling binary), in its own process group, with `STUDIO_HOST=http://127.0.0.1:` and its client id. +- The child connects back to the shell's **hub**, an HTTP/WebSocket server the shell binds on the first free loopback port in 8765–8785 (`WmHub::start`), and speaks Makepad's studio protocol (`AppToStudio` / `StudioToApp`). +- Frames reach the compositor without copies where the OS allows (implemented in Makepad `platform/src/os`): IOSurface on macOS, D3D11 shared handles on Windows, DMA_BUF on Linux with Vulkan and Wayland; a Linux OpenGL build reads every frame back through the CPU. +- A process app that dies takes only itself down: the shell removes its client and shows "App stopped". (Only in-process modules get the Restart face today; see [the disagreements](#where-the-code-and-the-adrs-disagree).) + +**In-process hosting** (`crates/shell/src/module_host.rs`): each instance of a module gets its own splash isolate (`alloc_splash_vm_with_network`) and storage namespace. The isolate separates the script heap only; the module's Rust code shares the shell's memory. Every call into a module runs under `catch_unwind` (`contain`, `contain_outside`): a panic marks that module failed, answers its in-flight tool calls "outcome unknown", closes its extra windows and shows a Restart face, while the shell keeps running ([#114](https://github.com/OctoSense-org/OctoSense/pull/114), ADR 0004 step 9). **On main.** + +### Script apps + +Script apps (the system apps News, Photos, Maps, Camera on phones, Mail, AI providers and YouTube, from `desktop/system-apps.json` and `phone/system-apps.json`, plus store apps) are OctoScript bundles. They all run in process, inside App Hub's **Card runner** (`CARD_MODULE` of `octosense-app-hub-app`): one nested isolate per app instance with `mod.res` and `mod.run` stripped, a jail and a quota. An app reaches the shell only through `host.request(".", …)` for the families its manifest was granted. A script bug fails inside its isolate. **On main.** + +```mermaid +flowchart TB + subgraph desktop["Desktop (macOS)"] + ds["OctoSense process
shell + App Hub + Card runner + Rinx"] + dk["octos child
OCTOS_APP_CORE_BIN serve --stdio"] + dt["Terminal child
--stdin-loop"] + ds -- "stdin/stdout" --> dk + dt -- "hub WebSocket, IOSurface frames" --> ds + end + subgraph android["Android (Home)"] + as["Home process
shell + App Hub + Card runner + Rinx (all in process)"] + ak["liboctos.so child
serve --stdio"] + as -- "stdin/stdout" --> ak + end + subgraph ohos["OpenHarmony"] + os["Home process
shell + embedded octos task"] + end + subgraph ios["iOS"] + is["Home process
no kernel"] + end +``` + +## 2. Agents + +**One kernel per shell; agents are sessions, not processes.** Inside the one kernel, every agent is an octos session run as tokio tasks. Tools such as file edits run in the kernel; octos's command tools run as short child processes in the session's workspace (OctoSense denies them to the system agent, [section 4](#4-tools-and-grants)). + +| Agent | What it is | Status | +| --- | --- | --- | +| **The system agent** | the session `_main:api:octosense#system` on the `_main` profile (`SYSTEM_SESSION` in `crates/kernel/src/network.rs`). It owns every app peer and supervises them. The person reaches it today through a Talk to Octos client; the shell draws no chat for it yet | On main | +| **An app agent** | one host-owned octos **peer** per (app, account), owned by the system agent (octos UPCR-2026-034, Rinx [ADR 0007](https://github.com/hagency-org/Rinx/blob/main/docs/adr/0007-host-owned-octos-app-peers.md)) | On main for Rinx (native) and for script apps behind a switch; see below | + +Each app peer has, separately from every other: + +- a **workspace**: the kernel-provisioned folder the peer's session is bound to. ADR 0004 §11 makes it the app's account folder (`apps//accounts//`); the broker does not pass that folder to the kernel yet (planned, step 10; see [the disagreements](#where-the-code-and-the-adrs-disagree)); +- a **memory namespace** `app//acct-` (`app_namespace()` in `crates/app-peers/src/broker.rs`); a kernel that does not return it is refused; +- its own **transcript** and **model** lane (the host sets the model with `peer/prepare` / `peer/model/set`); +- its own **tool list**: today the kernel's peer-safe defaults; the app's own tools and grants arrive with host-registered tools (octos [#2567](https://github.com/octos-org/octos/pull/2567), open; OctoSense plan step 6); +- **request contexts** (`peer/context/open`): one per client instance (a Rinx mini app, a card thread), each with its own transcript, folder `contexts//` inside the peer's workspace and child memory namespace. A context cannot read the files beside it. + +Who gets a peer today (`crates/ai-host/src/lib.rs`, `Policy::shipped()`; `crates/app-peers/src/hosted.rs`, `effective_services` = declared ∩ supported ∩ policy): + +- **Rinx**, the only native app granted the assistant (the four `octos.*` services), after first-use consent. +- **Script apps** that declare `octos.*`: one peer `card.` each, account `device` (`crates/ai-host/src/contained.rs`, [#106](https://github.com/OctoSense-org/OctoSense/pull/106)), only while `Policy::contained_apps` is on (off in the shipped policy; `OCTOSENSE_CONTAINED_APPS=1` turns it on) and the person consented at first use. +- **AppCard** (opt-in) takes its own kernel connection and sessions, not a peer. +- Process apps get no agent yet: the offer is made only in `ModuleHost::create` (the peer link is planned, step 8). + +Peers are kept, not thrown away: the broker resumes a peer with its host token (stored under `/../app-peers`, 0600), and never calls `peer_close` on sign-out, because octos cannot resume a closed peer or create a replacement for its (app, account) (ADR 0004 §11). + +### What a kernel crash means + +Every agent is in the one kernel, so a kernel crash stops **all** of them at once: the system agent and every app peer, mid-turn. The shell and its apps keep running; `availability()` reports the assistant failed, apps' ordinary UI keeps working, and in-flight turns end with the connection. Nothing is lost that octos had persisted: sessions, blackboards, memory namespaces and peer bindings are in the kernel's data directory, so the next `connect()` starts a new generation and consumers reopen and resume their peers and contexts with the stored host tokens. A tool call whose outcome is unknown is never retried without the person (ADR 0004 §7). + +## 3. Communication + +```mermaid +flowchart LR + subgraph shell["Shell (host connection)"] + broker["app-peers broker"] + relay["tool/approval relay
(seam, awaits octos#2567)"] + end + subgraph kernel["octos kernel"] + sys["system agent"] + p1["Calendar peer"] + p2["Mail peer"] + bb[("blackboard
peers/<slug>/result.md, turns.txt")] + end + rinx["Rinx (module)"] -- "OctosAppService" --> broker + card["script app"] -- "host.request(octos.*)" --> broker + proc["process app"] -. "peer link (planned)" .-> broker + broker -- "OUP: peer/prepare, peer/context/open, turn/start" --> kernel + sys -- "peer_send_input" --> p1 + p1 -- "writes" --> bb + sys -- "peer_gather / peer_list" --> bb + kernel -. "peer/input, peer/tool/call (octos#2567)" .-> relay +``` + +### OUP between the kernel and its clients + +The kernel speaks the **octos UI protocol** (OUP; `octos-ui/v1alpha1`, JSON-RPC 2.0 frames; octos [`api/OCTOS_UI_PROTOCOL_V1_SPEC_2026-04-24.md`](https://github.com/octos-org/octos/blob/main/api/OCTOS_UI_PROTOCOL_V1_SPEC_2026-04-24.md)). **On main.** + +- **Default: stdio.** The shell is the only client, over the child's stdin and stdout (newline-delimited JSON; octos UPCR-2026-016). Inside the shell, `crates/kernel/src/router.rs` multiplexes one frame stream between native consumers: each request gets a kernel-unique id and its reply goes back to that consumer only; notifications go to the consumers that named the session. +- **Talk to Octos: host-managed WebSocket** ([ADR 0003](adr/0003-shared-octos-client-access.md), [#98](https://github.com/OctoSense-org/OctoSense/pull/98); octos [`docs/HOST_MANAGED_SERVE.md`](https://github.com/octos-org/octos/blob/main/docs/HOST_MANAGED_SERVE.md), UPCR-2026-036). When the person turns it on in AI providers (`/external-access.json`), the shell restarts the kernel as `octos serve --host-managed --host 127.0.0.1`, on a listener the shell binds once and passes down (`--listen-fd` on Unix). It writes two tokens as the first two lines of the kernel's stdin, never in the environment: + + | Token | Holder | May | + | --- | --- | --- | + | **Host token** | the shell process only; minted per shell lifetime, never logged | everything (admin); native consumers connect with it to `/api/ui-protocol/ws` | + | **External token** | a paired web client (8-character code on the trusted sheet, 5 minutes, one claim) or a terminal client of this user (the 0600 `client-connection.json`) | only `/api/ui-protocol/ws`, as `_main`, and there only an allowlist of methods: open and read the system conversation, start, steer or interrupt its **own** turns, answer its own turns' approvals (once-only) and questions | + + External clients may not: call any `peer/*` method or name an app peer's session (`peer-…`, `peerctx-…`); set `cwd`, `topic` or `sandbox`; touch providers, keys, models, skills, snapshots or `server/shutdown`; use REST or admin routes. Their turns get a fixed tool set (file tools, `web_search`, `web_fetch`, memory reads, `ask_user_question`, media viewing; mirrored in `EXTERNAL_TURN_TOOLS` in `crates/kernel/src/system_tools.rs`): no shell, no peer tools, no host-routed tools (octos [#2601](https://github.com/octos-org/octos/pull/2601)). Not on OpenHarmony or iOS; Android unverified. + +### System agent and app agents, inside the kernel + +octos's peer mechanism, depth 1 (peers cannot create, steer or close peers). **On main** in octos; the tools are in octos `crates/octos-agent/src/tools/`. + +- **System agent → app agent.** `peer_send_input` (originator only, at most 64 KB) delivers text as the peer's next user turn, through the peer's inbox (a durable queue in serve, drained every few seconds, at least once). +- **App agent → system agent: the blackboard.** Each peer turn writes `peers//result.md` (plus `result-.md`) and a line in `turns.txt`; the system agent reads them with `peer_gather` and `peer_list` (`awaiting_input` shows a peer waiting on a question). It is the only cross-peer channel. +- **Questions.** A peer asks with `ask_user_question`; the system agent answers with `peer_respond`. `peer_respond` never answers approvals (octos refuses). Host-driven turns lose `ask_user_question`, so ADR 0004 §6 adds a shell tool **`host.ask`** that the shell routes to the system agent or to the app's conversation. **Planned** (step 6). + +### The host-owned path: `peer/input` + +For a host-owned peer, a plain `peer_send_input` runs as a kernel continuation without the app's tools or memory. ADR 0004 §6 fixes that: octos delivers the system agent's input to the host's driving connection as **`peer/input {peer, session_id, input_id, turn_id, text}`**, and the shell starts the turn itself (`turn/start` with the kernel's `turn_id`), so it runs with the app's tools, memory and context and its approvals surface in the app. If no host connection holds the peer, octos tells the system agent the app is not connected. **In progress**: the kernel side is octos [#2567](https://github.com/octos-org/octos/pull/2567) (open, draft; not on octos `main`); the shell side is plan step 6 and is not on OctoSense `main` (no `peer/input` handler exists). + +### An app and its own agent + +| Hosting | Channel | Status | +| --- | --- | --- | +| In-process native module (Rinx) | the **injected service**: `ai_host::offer` before `create`, `octosense_app_peers::injection::claim` inside it, giving a scoped `OctosAppService` (`Open`, `History`, `Turn`, `Interrupt`, `Approval`); the module never sees the protocol | On main | +| Script app | `host.request("octos.session.open" / "octos.session.history" / "octos.turn.start" / "octos.turn.interrupt", …)` to the `octos` host service (`crates/ai-host/src/contained.rs`); gated by the manifest, `Policy::contained_apps` and first-use consent (`consent_for_contained`); tool approvals its peer raises are declined and listed in `denied_approvals` | On main ([#106](https://github.com/OctoSense-org/OctoSense/pull/106), consent from [#120](https://github.com/OctoSense-org/OctoSense/pull/120)) | +| Native app in its own process | the **peer link**: its own channel on the app's hub connection (`PeerRequest`, `PeerReply`, `PeerToolCall` with the identity and caller stamped by the shell), never registered with the AI bus; client API in Makepad's `makepad-ai-services` | Planned (ADR 0004 §5, step 8); nothing in `hub.rs` yet | + +### Makepad's AI services bus vs OctoSense's app agents + +Two models live side by side (ADR 0004, "Two AI models"): + +| | Makepad AI services bus | OctoSense app agents | +| --- | --- | --- | +| Where | the window manager's half in `crates/shell/src/ai_bus.rs`; upstream `libs/ai/services` | `crates/ai-host`, `crates/app-peers`, the octos kernel | +| Shape | **one central conversation** (the desktop's AI pane, Makepad's `aichat`) calls typed tools that apps register with a risk level (`Read`, `Act`, `Destructive`) | **one agent per app** with the app's full context (workspace, memory, history, tools), supervised by the system agent | +| Routing | the shell stamps each up-frame with the sender's endpoint, forwards registrations to the pane (replayed on reconnect), routes the pane's calls to the app's socket, and answers the `os` service (list, launch, focus, close, open) itself | the broker talks OUP to the kernel directly | +| Used for | the AI pane on the desktop; Rinx's assistant tools today; the Terminal's `run`. Its `confirm: host` calls go through the shell's approval router ([#120](https://github.com/OctoSense-org/OctoSense/pull/120)) | the apps' own assistants; delegation from the system agent | + +**Why app-agent traffic does not use the bus.** The bus is a narrow, one-way API to a central agent that holds all the context; an app agent needs its app's full context and a private, supervised session, and the system agent must address it through the kernel's peer mechanism. The bus also carries no account, request context or caller, so it cannot stamp the identity ADR 0004 §5 requires on every tool call, and its registrations are visible to the pane. So in OctoSense the bus is not the system agent's channel to apps; it stays for upstream Makepad apps (ADR 0004 §6), and the peer link for process apps is deliberately a separate channel. Neither `crates/ai-host` nor `crates/app-peers` uses the bus. + +## 4. Tools and grants + +**The manifest declares, the person grants at install, the shell enforces on every call** (ADR 0004 §12, ADR 0002 §4). A script app declares capabilities in `manifest.json` and its tools in `tools.json` (admitted and pinned by App Hub); a native app declares them in its reviewed `native-apps.json` entry (`agent.octos`, `agent.tools`, `agent.tool_policy`). + +Where an agent's tools come from: + +| Source | Example | Runs | Status | +| --- | --- | --- | --- | +| The app's own tools (`tools.json`: name `.`, schemas, `risk`, `confirm: host` or `app`, `shareable`) | `mail.send`, `rinx.message.send` | the app's host service, module or process, called by the shell | Planned for agents (step 6, needs octos#2567). Rinx's tools reach the AI pane over the bus today | +| The system toolbox, by capability (`research`, `crawl`, `model`) | `toolbox.search`, `toolbox.web_read`, `toolbox.deep_crawl`, `workflow.run` | host | In progress ([#108](https://github.com/OctoSense-org/OctoSense/pull/108)); `model` not registered yet | +| octos's generic tools | file reads fenced to the workspace, memory, `web_search`, `deep_search` | octos | On main as octos's peer-safe defaults; per-app `generic_tools` lists planned (step 6) | +| Other apps' shareable tools, routed by the shell | Calendar's agent calling `mail.send` | the owning app, via the shell | Planned (ADR 0004 §7, step 6) | +| Command execution | `terminal.run` (the Terminal's shareable tool: `confirm: host`, `auto_approvable: false`) | host tool, in a terminal the person sees | Planned as a host tool (steps 6 and 11); the constant exists (`COMMAND_EXECUTION_TOOL`) | + +**The system agent's tool set** (`crates/kernel/src/system_tools.rs`, [#117](https://github.com/OctoSense-org/OctoSense/pull/117)). **On main**, partly enforced: + +- Its default list, `SYSTEM_AGENT_TOOLS`: supervision (`peer_send_input`, `peer_gather`, `peer_list`, `peer_respond`, `peer_close`; not `peer_handoff`), its workspace's file tools, `ask_user_question` and media viewing, memory, `web_search` / `web_fetch`, `tool_search`. Grants add to it (`SystemAgentTools`: toolbox tools, other apps' shareable tools, command execution). +- **octos's own shell is never offered.** Before every kernel start `enforce` writes the `_main` profile's `tool_policy`, denying `group:runtime` (`shell`, `bash`, `exec_command`, `write_stdin`). It replaces only a policy OctoSense wrote and refuses the person's own octos home. +- **Not yet exact.** octos has no per-session tool list the host can set, so the system agent gets every other tool octos registers; the exact-list test is kept but ignored until octos#2567. +- **Command execution from Settings** (off by default, each command approved live) is a seam: `SystemAgentTools::grant_command_execution` exists; no Settings switch sets it and host tools cannot be registered before octos#2567. + +**External clients** get only octos's fixed allowlist ([section 3](#oup-between-the-kernel-and-its-clients)); host-routed tools, developer grants and `dev.run` never reach them. + +## 5. Approvals + +**A grant is not an approval.** A grant says an agent may *have* a tool; an approval says *this* call, with these exact arguments, may run. Read and in-app act tools run once granted; an outward or destructive call (send, post, share, buy, delete, run a command) needs the person, live or by a standing rule. Only the person approves; the system agent never does, and an agent's own text is never an approval surface (ADR 0004 §8). + +**The approval router** (`crates/shell/src/approvals/router.rs`, [#120](https://github.com/OctoSense-org/OctoSense/pull/120)) is the shell's one place that answers approval requests, on their exact arguments. **On main.** For each request, in order: + +```mermaid +flowchart TB + req["Approval request
(owning app, tool, exact args, caller, context)"] + dev{"1. Developer mode
covers the app?"} + capp{"2. confirm: app?"} + always{"3. auto_approvable: false,
outcome unknown,
or an external client?"} + rule{"4. A standing rule on
(owning app, tool) matches?"} + sheet["5. Shell-drawn sheet
(one per request, or batched
in the system chat)"] + ok(["approved, audited"]) + appsheet["The owning app's own sheet,
showing the caller
(waits, then refused visibly)"] + person["The person"] + req --> dev + dev -- yes --> ok + dev -- no --> capp + capp -- yes --> appsheet --> person + capp -- no --> always + always -- yes --> sheet + always -- no --> rule + rule -- yes --> ok + rule -- no --> sheet + sheet --> person +``` + +1. **Developer mode** (`dev_hooks.rs`, `crates/shell/src/dev_mode.rs`, [#118](https://github.com/OctoSense-org/OctoSense/pull/118)) approves everything of the apps it covers, `auto_approvable: false` and `confirm: app` included, never for an external client. Only the person turns it on: a development build with `OCTOSENSE_DEV_MODE=all` (or a list of apps), a release build only with `--dev-grant-all`, or the desktop's Developer options with a typed phrase; store builds never. It shows a banner, audits every call, and ends after 8 hours or at restart outside a developer profile. `dev.run` is not registered yet (step 11). +2. **`confirm: app`** tools go to the owning app's own sheet (an `AppConfirm` the app registers with `register_app_confirm`), with the caller; no rule answers them. An app that has not registered one gets 120 s (`app_wait_s`), then the call is refused visibly. No app registers one on `main` yet; Rinx's send sheet still answers inside Rinx. +3. **`auto_approvable: false`**, **outcome unknown** and **external clients'** calls always go to the person. +4. **Standing rules** (`rules.rs`), keyed on **(owning app, tool)** whoever calls, with conditions on the exact arguments (recipients in contacts or in the thread, no attachments, triggered by the person, count or amount limits; a condition fails when the call lacks the fact), a daily cap (20 by default for a tool rule), a time box of at most 60 minutes for the broadest rule ("everything this app asks for the next hour"; there is no "everything, forever"), and one "all rules off". The shell has no contacts source yet, so "recipients in contacts" never matches today. Runs started by incoming content are skipped unless a rule opts in. The person creates rules in Settings → Assistant → Approvals (`settings_page.rs`) or from a sheet; the system agent may only suggest one. +5. Otherwise a **shell-drawn sheet** (`sheet.rs`, `view.rs`): each line shows the owning app, the tool, the exact arguments and, for a cross-app call, the calling app; the system agent's `confirm: host` approvals of one request may be batched into one sheet in its chat. + +Every decision reaches the relay once and the **audit** (`audit.rs`: one JSON line per decision in `/logs/approvals-audit.jsonl`, owner-only, with a digest of the arguments rather than the arguments; developer mode keeps its own full audit in `logs/dev-audit.jsonl`); every automatic one is also a notification. Rules are in `approvals/rules.json`, consent in `approvals/consent.json`, under the OctoSense home. + +**First-use consent** (`consent.rs`): the first time an app asks for its agent, the shell shows what the agent may read and use and where the model runs; Settings lists every app's agent with an off switch (ADR 0004 §4). **On main** for native modules (`consent_for_module`, before the offer in `module_host.rs`) and script apps (`consent_for_contained`). + +**What feeds the router today: only the AI services bus.** The pane's calls to `confirm: host` tools listed in `native-apps.json` (today the Terminal's `run`) are held (`bus::`) until the router answers. The octos side (`peer/tool/call` and kernel approvals into `relay.rs`) is a seam waiting for octos#2567; `set_relay` is not called on `main`. Until then octos approvals raised in an app's context go to that app: script apps' are declined in `contained.rs`, Rinx's are answered by the person in Rinx, and in developer mode `crates/app-peers/src/host_approvals.rs` answers them for the covered apps (audited). + +## 6. Storage and secrets + +One host-owned layout for every app, declared by its manifest (`storage` block) and computed only by the shell (`crates/shell/src/app_storage/`, [#115](https://github.com/OctoSense-org/OctoSense/pull/115), ADR 0004 §11). **On main** as an API native modules are offered at `create` (`octosense_app_peers::storage`, like the assistant service): + +``` +/apps// the app's jail (App Hub's jail root; a native app's sandbox root) + accounts// one per account ("device" when the app has none): + the account's data = that account's agent workspace + common/ app data not tied to an account + cache/ evictable, not backed up +/secrets// host-owned: tokens, keys, passwords, encryption stores +``` + +- **The OctoSense home** is the platform's app data directory on a phone, else `~/.octosense` (`OCTOSENSE_HOME` overrides; `crates/shell/src/octosense/paths.rs`). The apps root may be moved with `OCTOSENSE_APP_DATA`; the secrets root is always `/secrets`, and the two never overlap. Directories are 0700 and symlinked components are refused. +- **The account hash** is 128 bits of a domain-separated SHA-256 of the normalized account id (`account_hash`). +- **The agent workspace is the account folder**, by the ADR. Today the folder exists and a module gets it from `AppStorage::agent_workspace`, but the broker does not yet hand it to `peer/prepare` as the peer's `cwd`: peers work in the kernel-provisioned workspace (planned, step 10). +- **Secrets** (`secrets.rs`, `AppStorage::secrets`): the keychain on macOS and iOS (one item per profile, app and key), elsewhere one owner-only (0600) plaintext file per key in `secrets//`; the file store also for tests, headless runs and `OCTOSENSE_SECRETS=file`. The Windows, Linux and Android vaults are a TODO. Secrets are never under `apps/`. +- **The startup check** (`check.rs`): no agent workspace may contain or reach `secrets/` (a symlinked workspace or jail, a symlink or hard link into the secrets, the secrets root inside it). A flagged workspace is refused (`agent_workspace` answers `Refused`, for one account or all) until a later start finds it clean; startup continues and nothing is deleted. +- **Sign-out** suspends an account's agent and never closes it: `Storage::sign_out` makes the workspace answer `SignedOut`. It is a seam today (the shell has no account events yet; a module binds its account through `OctosAppService::set_account`); answering tool calls with `signed_out` is planned (step 6). +- **The kernel's own data** is separate: the core dir `/octos-home/.octos` (desktop `~/.octosense/octos-home/.octos`, `OCTOS_APP_CORE_DIR` overrides; `crates/kernel/src/dirs.rs`), holding the `_main` profile, sessions, blackboards and memory namespaces. Provider keys are the `llm` service's (macOS keychain, else owner-only files; see [ai-services.md](ai-services.md#ai-providers-and-the-llm-host-service)). Peer host tokens are in `/../app-peers` (0600). +- **Script apps** keep App Hub's jail and quota; their secrets live behind host services and host sheets ("Secrets are the host's", AGENTS.md rule 3). Rinx keeps its own data folder until its data moves under this layout ([hagency-org/Rinx#37](https://github.com/hagency-org/Rinx/issues/37)); under an explicit `OCTOSENSE_HOME` the shell points it at `/apps/rinx/data` (`RINX_DATA_DIR`, set in `crates/shell/src/octosense/paths.rs`; an explicit `RINX_DATA_DIR` wins). + +## 7. Trust boundaries and isolation + +``` + person ── host sheets (keys, PINs, approvals) ──┐ + v + +------------------------ shell process (trusted) -------------------------+ + | holds: host token, peer host tokens, provider keys (via llm), secrets | + | checks every call: grants, consent, approvals, audit | + | +------------------+ +------------------------------------------+ | + | | native modules | | Card runner: script apps in isolates | | + | | reviewed, same | | (jail, quota, host.request by grant) | | + | | memory: trusted | +------------------------------------------+ | + | +------------------+ | + +-------|------------------------------------------|-----------------------+ + | hub (loopback) | OUP, host token + +-------v----------+ +--------v--------------------+ + | process apps | | octos kernel | + | (OS sandbox: | | workspace fence per peer | + | planned) | | external clients: allowlist| + +------------------+ +-----------------------------+ +``` + +| Boundary | What holds it | Status | +| --- | --- | --- | +| Script app ↔ shell | App Hub's nested isolate: no `mod.res` / `mod.run`, a jail and quota, only `host.request` for granted families; password fields inert; secrets on host sheets | On main | +| Native module ↔ shell | none in memory: reviewed first-party code only (`native-apps.json`); **panic containment** at the module boundary (`catch_unwind`, [#114](https://github.com/OctoSense-org/OctoSense/pull/114)); limits: a panic while unwinding, `panic = "abort"`, FFI | On main | +| Process app ↔ shell | its own address space; an **OS sandbox** from the manifest's `sandbox` and `storage` (macOS sandbox profile, Linux Landlock and seccomp, Windows AppContainer) | Sandboxes planned (ADR 0004 §3, step 8); today a process app runs with the user's rights | +| App ↔ kernel | apps never speak the kernel protocol and **never see the host token**: modules get a scoped `OctosAppService`, script apps a host service, process apps (planned) the peer link | On main | +| Peer ↔ peer | octos: separate workspace (overlapping workspaces refused), memory namespace, transcript; request contexts fenced to `contexts//` | On main (octos UPCR-2026-034) | +| Agent ↔ secrets | secrets outside every jail and workspace; startup check | On main | +| External client ↔ kernel | external token, method and tool allowlists, `Host` and origin checks, no peer access | On main ([ADR 0003](adr/0003-shared-octos-client-access.md)) | + +**What the shell checks on every call** (ADR 0004 §3): grants and consent; budgets, rate limits and background policy; each tool call's name and arguments against the declared `tools.json`; each result against its schema and size; its own audit; crash cleanup. On main today: consent, the `octos.*` service grants by exact name (declared ∩ supported ∩ policy), argument rules and size caps for script apps (text at most 32 KiB, replies at most 2 MiB), the approval router and audit for bus calls. Tool-call schema checks and budgets come with host-registered tools (step 6). + +**What nobody can check**: what a native app's code does inside its own tool, or why it starts a turn. Review and, for process apps, the sandbox are the controls. + +## 8. Worked example: emailing a meeting invite + +"Ask the system agent to email a meeting invite." This is ADR 0004's Calendar and Mail example. **Almost all of it is planned**: there is no Calendar app on `main`, Mail declares no `tools.json` or `octos.*` yet, and the kernel side (`peer/input`, host-registered tools) is octos#2567. The status column says which pieces exist. + +```mermaid +sequenceDiagram + actor P as Person + participant S as System agent + participant SH as Shell (host connection) + participant C as Calendar agent (peer) + participant R as Approval router + participant M as Mail (host service) + P->>S: "Invite Ana, Bo and Edward to Tuesday 3 pm" + S->>C: peer_send_input (brief) + Note over S,SH: octos delivers peer/input to the host + SH->>C: turn/start (app's tools, memory, context) + C->>SH: peer/tool/call calendar.create_event + SH-->>C: result + C->>SH: peer/tool/call mail.send x3 (caller: Calendar) + SH->>SH: grant check: Calendar granted mail.send? + SH->>R: approval (Mail, mail.send, exact args, caller Calendar) + alt standing rule on (Mail, mail.send) matches + R-->>SH: approved (notified, audited) + else no rule + R->>P: one batched sheet with the three invitations + P-->>R: approve + end + SH->>M: execute mail.send x3 + M-->>SH: results + SH-->>C: peer/tool/result + C->>C: octos writes peers/(slug)/result.md + S->>S: peer_gather reads the result + S->>P: "Booked Tue 3 pm; invitations sent to 3" +``` + +| Step | What happens | Status | +| --- | --- | --- | +| 1 | The person asks the system agent, in the system conversation `_main:api:octosense#system`. | Reachable today only from a paired Talk to Octos client; the shell has no chat of its own for the system agent yet (the desktop's AI pane is Makepad's `aichat` on the bus, not the system agent) | +| 2 | The system agent plans. Ambiguity is asked, not guessed ("two Edwards?"); for a bounded read it may call a granted tool directly. Work that needs Calendar's judgement goes to Calendar's agent with `peer_send_input`. | `peer_send_input` on main; direct cross-app grants planned (step 6) | +| 3 | octos delivers the input to the shell as `peer/input`; the shell starts the turn on Calendar's peer with `turn/start`, so it has Calendar's tools, memory and account context. If Calendar's peer has no host connection, the system agent is told the app is not connected. | In progress ([octos#2567](https://github.com/octos-org/octos/pull/2567)); shell side planned (step 6) | +| 4 | Calendar's agent calls `calendar.create_event` (its own tool) and Mail's shareable `mail.send` once per invitee. Each call reaches the shell as `peer/tool/call`; the shell stamps the caller (Calendar's agent), account and context. | Planned (ADR 0004 §5, §7; step 6) | +| 5 | The shell checks that Calendar's manifest was granted `mail.send` (at install for a script app); no second, agent-level consent. | Planned (step 6) | +| 6 | `mail.send` is outward and `confirm: host`, so the approval router takes it: developer mode off; not `confirm: app`; not `auto_approvable: false`; a standing rule on (Mail, `mail.send`) such as "send to my contacts" approves it (notified and audited; the contacts condition needs a contacts source the shell does not have yet), else one batched shell sheet shows each invitation: owning app Mail, tool `mail.send`, calling app Calendar, the exact arguments. | Router, rules, sheets and audit on main ([#120](https://github.com/OctoSense-org/OctoSense/pull/120)); fed by kernel tool calls once octos#2567 lands | +| 7 | On approval the shell hands each call to Mail's host service, which sends with the account the person signed in to on Mail's host sheet (the password never reaches the agent), and returns the result to the kernel as `peer/tool/result`. | Mail's host service on main (`apps/mail/host-service`); its tools as agent tools planned | +| 8 | Calendar's turn ends and octos writes `peers//result.md` and `turns.txt`; the system agent reads it with `peer_gather`. A failed invitation is named, and one with an unknown outcome is never retried without the person. | Blackboard on main (octos) | +| 9 | The system agent announces "Booked Tue 3 pm; invitations sent to 3". Calendar's and Mail's own UIs show the change because their data changed. | Planned | + +## Where the code and the ADRs disagree + +Found while writing this page (2026-09-28); none is fixed here, as this change is documentation only. + +1. **Process apps beyond the Terminal.** ADR 0004 §2 says the Terminal is the only process app for now and that Linux without Vulkan runs every app in process. `native-apps.json` declares the opt-in Sheets and Reference as `process` on macOS, Windows and Linux (plain `process`, not `process-if-vulkan`), so an OpenGL or X11 Linux build would run them as processes with CPU readback. +2. **Restart of a dead process app.** ADR 0004 §2 says its tile shows it closed with a restart. The shell removes the client and posts "App stopped"; the Restart face exists only for in-process modules (`module_view.rs`). +3. **Agent workspace = account folder.** ADR 0004 §11 names it the `peer/prepare` `cwd`. The broker sends no `cwd` to `peer/prepare` and binds the peer to the kernel-provisioned workspace; the storage API's `agent_workspace` is not connected to it. The account folder name (`account_hash`, SHA-256) and the memory namespace tag (FNV-1a in `broker.rs`) are also different hashes of the account. +4. **ADR 0003 "What the profile runs"** says OctoSense configures neither the tool set nor the sandbox. Since [#117](https://github.com/OctoSense-org/OctoSense/pull/117) the shell writes a `tool_policy` denying `group:runtime` into the `_main` profile before every start, so the host's own turns have no octos shell either. +5. **Stale context lines.** ADR 0004's context table and the root README's layout table still mention native News, Maps and Photos comparison modules (`apps/*/native`); ADR 0004 step 1 deleted them. [ai-services.md](ai-services.md) is dated 2026-09-27: its architecture diagram shows "no `octos.*` service" for the Card runner (merged in #106), and it says contained apps are off "until first-use consent lands" (consent landed in #120; the `Policy::contained_apps` switch is still off by default). +6. **Command execution "turned on in Settings"** (`crates/kernel/README.md`, `system_tools.rs` docs): no Settings switch exists yet; `SystemAgentTools::grant_command_execution` is a seam. +7. **`host::processes_available()`'s test** checks only `wasm32` while the function also excludes native mobile. +8. **Approvals, ADR 0004 §8.** Every app tool call is to reach the router through `peer/tool/call`; today only the AI bus feeds it (above). No app registers its own `confirm: app` sheet, so such a call would wait and be refused. The audit keeps a digest of the arguments, not the arguments. The send queue and undo window are not built. +9. **Storage, ADR 0004 §11.** Secrets use the OS keychain only on macOS and iOS (plaintext 0600 files elsewhere). The startup check refuses workspaces that reach the secrets through links or containment rather than looking for a `secrets/` path, and does not stop startup. `storage.accounts` defaults to `false` in `spec.rs` but `consent.rs` treats a missing value as `true`. +10. **Developer mode, ADR 0004 §13.** `dev.run` is not registered; Settings turns it on only for all apps (a chosen list only through `OCTOSENSE_DEV_MODE`); there is no phone gesture; in-process modules still show their own confirmation sheets. + +## Source map + +| What | Where | +| --- | --- | +| Native app manifest, generator, generated table | [`native-apps.json`](../native-apps.json), [`tools/native_apps.py`](../tools/native_apps.py), [`crates/shell/src/native_apps.rs`](../crates/shell/src/native_apps.rs) | +| Hosting decision, host services | [`crates/shell/src/apps.rs`](../crates/shell/src/apps.rs) (`AppRegistry::hosting`, `register_host_services`) | +| Process apps, hub | [`crates/shell/src/clients.rs`](../crates/shell/src/clients.rs), [`crates/shell/src/hub.rs`](../crates/shell/src/hub.rs), [`crates/shell/src/host.rs`](../crates/shell/src/host.rs) | +| In-process modules, panic containment | [`crates/shell/src/module_host.rs`](../crates/shell/src/module_host.rs), [`crates/shell/src/module_panic_tests.rs`](../crates/shell/src/module_panic_tests.rs) | +| AI services bus (shell half) | [`crates/shell/src/ai_bus.rs`](../crates/shell/src/ai_bus.rs) | +| Approvals, consent, audit, Settings page | [`crates/shell/src/approvals/`](../crates/shell/src/approvals/mod.rs) | +| Developer mode | [`crates/shell/src/dev_mode.rs`](../crates/shell/src/dev_mode.rs) | +| App storage, secrets, startup check | [`crates/shell/src/app_storage/`](../crates/shell/src/app_storage/mod.rs) | +| Kernel service: launch, dirs, Talk to Octos, frame router, system agent tools | [`crates/kernel/src/`](../crates/kernel/README.md) (`launch.rs`, `dirs.rs`, `network.rs`, `router.rs`, `system_tools.rs`) | +| Shell AI entry point, policy, offer; script apps' `octos` service | [`crates/ai-host/src/lib.rs`](../crates/ai-host/src/lib.rs), [`crates/ai-host/src/contained.rs`](../crates/ai-host/src/contained.rs) | +| App peers: broker, hosted launch, storage contract, injection | [`crates/app-peers/src/`](../crates/app-peers/README.md) (`broker.rs`, `hosted.rs`, `storage.rs`, `injection.rs`, `host_approvals.rs`) | +| Script system apps per shell | [`desktop/system-apps.json`](../desktop/system-apps.json), [`phone/system-apps.json`](../phone/system-apps.json) | +| octos: host-managed serve, UPCRs, peer tools | [`docs/HOST_MANAGED_SERVE.md`](https://github.com/octos-org/octos/blob/main/docs/HOST_MANAGED_SERVE.md), [UPCR-2026-034](https://github.com/octos-org/octos/blob/main/docs/OCTOS_UI_PROTOCOL_CHANGE_REQUEST_UPCR_2026_034_HOST_APP_PEERS.md), UPCR-2026-035 ([octos#2567](https://github.com/octos-org/octos/pull/2567), not on `main`), [UPCR-2026-036](https://github.com/octos-org/octos/blob/main/docs/OCTOS_UI_PROTOCOL_CHANGE_REQUEST_UPCR_2026_036_HOST_MANAGED_SERVE.md), [`crates/octos-agent/src/tools/`](https://github.com/octos-org/octos/tree/main/crates/octos-agent/src/tools) | diff --git a/docs/architecture.zh-CN.md b/docs/architecture.zh-CN.md new file mode 100644 index 00000000..76e4c6c3 --- /dev/null +++ b/docs/architecture.zh-CN.md @@ -0,0 +1,456 @@ +# OctoSense 架构 + +[English](architecture.md) | 简体中文 + +OctoSense 的各部分如何组合在一起:每个平台上运行哪些进程、Agent 在哪里、各部分如何通信、工具如何授权和审批、数据与机密存放在哪里,以及信任边界在哪里。本文描述 2026-09-28 的 `main`(OctoSense `ff40e9c`,它锁定 octos `5e7577f0`),内容均在代码中读过。 + +每条陈述都标明状态: + +- **已在 main**:已合入,在本仓库代码中读过(给出路径)。 +- **进行中**:有未合并的 pull request,附链接。 +- **规划中**:已在 ADR 中决定(附链接和计划步骤),尚未实现。 + +背后的决策是 [ADR 0001](adr/0001-one-octosense-repository.md)(英文,一个仓库)、[ADR 0002](adr/0002-event-driven-app-agents.md)(英文,应用 Agent,Proposed,经 0004 修订)、[ADR 0003](adr/0003-shared-octos-client-access.md)(英文,Talk to Octos)和 [ADR 0004](adr/0004-native-apps-hosting-and-peers.md)(英文,原生应用、应用 Agent、跨应用协作与审批,已接受)。助手的各项服务、各类应用目前能调用什么、如何在本地运行,见 [ai-services.zh-CN.md](ai-services.zh-CN.md);本文不再重复。 + +## 目录 + +- [全局](#全局) +- [1. 各平台的进程](#1-各平台的进程) +- [2. Agent](#2-agent) +- [3. 通信](#3-通信) +- [4. 工具与授权](#4-工具与授权) +- [5. 审批](#5-审批) +- [6. 存储与机密](#6-存储与机密) +- [7. 信任边界与隔离](#7-信任边界与隔离) +- [8. 完整示例:用邮件发送会议邀请](#8-完整示例用邮件发送会议邀请) +- [代码与 ADR 不一致之处](#代码与-adr-不一致之处) +- [源码位置](#源码位置) + +## 全局 + +```mermaid +flowchart LR + person(["用户"]) + subgraph shellp["OctoSense Shell 进程(桌面端,或手机上的 Home)"] + wm["窗口管理器、启动器、面板
crates/shell"] + mods["进程内原生模块
App Hub、Rinx、(AppCard)"] + runner["App Hub Card runner
脚本应用各在隔离环境中"] + router["审批路由
crates/shell/src/approvals"] + aihost["ai-host + app-peers broker
到内核的宿主连接"] + bus["AI 服务总线
crates/shell/src/ai_bus.rs"] + end + term["进程应用(桌面端)
Terminal;可选 Sheets、Reference"] + subgraph kern["octos 内核(每个 Shell 一个)"] + sys["系统 Agent 会话
(profile _main)"] + peers["应用 peer
每个(应用,账号)一个"] + end + ext["Talk to Octos 客户端
网页、终端(需手动开启)"] + person --> wm + wm --- mods + wm --- runner + term <-->|"hub(回环 WebSocket)"| wm + mods -- "注入的 OctosAppService" --> aihost + runner -- "host.request(octos.*)" --> aihost + aihost <-->|"OUP over stdio
(或宿主 token 的 WebSocket)"| kern + sys -- "peer_send_input / 黑板" --> peers + ext -. "外部 token、允许列表" .-> sys + term -- "类型化工具" --> bus + mods -- "类型化工具" --> bus + bus --> router +``` + +``` ++--------------------------- OctoSense Shell 进程 -------------------------------+ +| 窗口管理器 / 启动器 / 面板 审批路由 AI 服务总线 | +| 进程内模块:App Hub(+ 运行脚本应用的 Card runner)、Rinx、(AppCard) | +| ai-host + app-peers broker == 宿主连接 ==+ | ++--------------------------------------------|-----------------------------------+ + ^ hub:回环 WebSocket | OUP:stdio(默认)或 + | | 带宿主 token 的 WebSocket ++-------+---------+ +--------------v--------------------------+ +| 进程应用 | | octos 内核(子进程;OHOS 上在进程内) | +| Terminal 等 | | 系统 Agent 会话 | ++-----------------+ | 应用 peer:每个(应用,账号)一个 | + | 工具以短时子进程运行 | + Talk to Octos 客户端 ----> +-----------------------------------------+ + (外部 token、允许列表、需手动开启) +``` + +## 1. 各平台的进程 + +### Shell + +每台设备一个 Makepad 进程:桌面端(`desktop/`,包 `octosense`)或手机上的 Home(`phone/`,包 `octosense-home`),两者都链接同一个 Shell crate `crates/shell`([ADR 0001](adr/0001-one-octosense-repository.md))。Shell 拥有窗口管理器、启动器、宿主面板、审批路由、应用存储以及内核的宿主连接。 + +### octos 内核 + +每个 Shell 最多运行一个 [octos](https://github.com/octos-org/octos) 内核,它是 [`crates/kernel`](../crates/kernel/README.zh-CN.md)(包 `octosense-kernel`)中的一项 Shell 服务,通过 [`crates/ai-host`](../crates/ai-host/README.md) 访问。**已在 main。** 运行方式(`crates/kernel/src/launch.rs`,`launch::resolve`): + +| 平台 | 内核 | 如何启动 | +| --- | --- | --- | +| 桌面端(macOS;Windows 和 Linux 未经测试) | 子进程:`OCTOS_APP_CORE_BIN`(或嵌入方的 `Options::program`)指定的 `octos` 二进制 | `serve --stdio --data-dir `;没有二进制就没有内核(不在 `PATH` 中查找)。随桌面端一起发布该二进制正在进行中([#85](https://github.com/OctoSense-org/OctoSense/pull/85))。 | +| Android(Home) | 子进程:APK 中打包的 `liboctos.so`,由 [`tools/kernel-artifact.py`](../tools/kernel-artifact.py) 按锁定的 octos 版本构建 | `serve --stdio`,在应用的原生库目录中、`libmakepad.so` 旁边找到 | +| OpenHarmony | 进程内:`octos_cli::embedded::serve_io` 作为一个任务运行在内存中的双工流上(HAP 不允许 exec) | 同样的协议,没有子进程 | +| iOS | 无 | 提供方配置会保存;没有应用能用上助手 | + +生命周期(`crates/kernel/src/lib.rs`、`kernel.rs`): + +- **按需启动,共享使用。** 第一个使用方调用 `connect()` 之前什么都不运行;之后的使用方连接到同一代内核。新一代内核会等上一代退出后再启动,从而先释放 octos 对数据目录的单写者锁。 +- **提供方变化时重启。** `llm` 宿主服务重写配置后调用 `restart()`:所有连接以 `CloseReason::Restarted` 结束,使用方重新连接到新的内核。 +- **空闲停止。** Talk to Octos 关闭时,最后一个连接关闭后内核即停止。开启时,内核一直运行,直到用户关闭它或 Shell 退出。 +- **随 Shell 退出。** Shell 在子进程整个生命周期内持有其 stdin;Shell 退出、崩溃或被杀死时,内核读到 EOF 并停止。在 `--host-managed` 模式下,octos 在 Linux 和 Android 上还会请求在父进程死亡时收到 SIGTERM(octos `crates/octos-cli/src/api/host_managed.rs` 中的 `bind_to_parent()`);子进程也以 `kill_on_drop` 方式创建。 +- **崩溃**会以 `CloseReason::Exited`(附最后几行 stderr)结束所有连接;不会自动重启,下一次 `connect()` 启动新一代。见[内核崩溃意味着什么](#内核崩溃意味着什么)。 + +### 原生应用:进程内还是独立进程 + +原生应用是第一方 Rust crate,只在 [`native-apps.json`](../native-apps.json) 中声明;`tools/native_apps.py` 据此生成 `crates/shell/src/native_apps.rs` 和各处 Cargo 配置块,并在 CI 中检查(ADR 0004 §1,计划步骤 1)。**已在 main。** 目前的清单: + +| 应用 | macOS / Windows 上的托管 | Linux | Android、iOS、OpenHarmony | 桌面端 / 手机 | 助手 | +| --- | --- | --- | --- | --- | --- | +| App Hub(`apphub`:商店和 Card runner) | 进程内 | 进程内 | 进程内 | 默认 / 默认 | – | +| Rinx | 进程内 | 进程内 | 进程内 | 默认 / 默认 | 获授权四个 `octos.*` 服务 | +| Terminal | **独立进程** | Vulkan 构建且在 Wayland 会话中时为独立进程,否则进程内 | 进程内 | 默认 / 关闭 | –(其 `run` 工具为 `confirm: host`、`auto_approvable: false`) | +| Sheets、Reference | 独立进程 | 独立进程 | 进程内 | 可选 / `mobile-apps` | – | +| AppCard | 进程内 | 进程内 | 进程内 | 可选 / 可选 | 自己的内核连接 | + +因此在 Shell 默认附带的应用中,**只有 Terminal 是进程应用**,而且只在桌面端。App Hub 留在进程内,因为它承载所有脚本应用运行所在的 Card runner;Rinx 留在进程内,直到 peer link 和进程沙箱就绪(ADR 0004 §2)。 + +Shell 在运行时如何决定(`crates/shell/src/apps.rs`,`AppRegistry::hosting`):如果目标平台不能运行进程(在 wasm 和原生移动平台上 `host::processes_available()` 为 false),所有应用都作为模块;App Hub 和 Settings 始终是模块;其他情况下原生应用按清单条目托管,并且只有存在进程形态(可以 `cargo run` 的源码检出,或同目录下的二进制)时才作为进程运行。发布包目前还不附带进程应用的二进制([#94](https://github.com/OctoSense-org/OctoSense/pull/94),进行中),因此在发布包中会回退到进程内。按应用的覆盖设置(`~/.makepad/wm/apps.splash`、`--module `)可以把模块切换为进程。 + +**进程托管**使用 Makepad 窗口管理器的托管机制(`crates/shell/src/clients.rs`、`hub.rs`): + +- Shell 以 `--stdin-loop` 启动应用子进程(在源码检出中为 `cargo run … -- --stdin-loop`,否则为同目录下的二进制),放在独立的进程组中,并设置 `STUDIO_HOST=http://127.0.0.1:` 和其客户端 id。 +- 子进程回连 Shell 的 **hub**:Shell 在回环地址 8765–8785 中第一个空闲端口上绑定的 HTTP/WebSocket 服务(`WmHub::start`),使用 Makepad 的 studio 协议(`AppToStudio` / `StudioToApp`)。 +- 在操作系统允许时,画面以零拷贝方式到达合成器(在 Makepad `platform/src/os` 中实现):macOS 上用 IOSurface,Windows 上用 D3D11 共享句柄,Linux 在 Vulkan 加 Wayland 下用 DMA_BUF;Linux 的 OpenGL 构建则每帧经 CPU 回读。 +- 进程应用崩溃只影响它自己:Shell 移除其客户端并显示 “App stopped”。(目前只有进程内模块有 Restart 界面;见[不一致之处](#代码与-adr-不一致之处)。) + +**进程内托管**(`crates/shell/src/module_host.rs`):模块的每个实例都有自己的 splash 隔离环境(`alloc_splash_vm_with_network`)和存储命名空间。隔离环境只隔离脚本堆;模块的 Rust 代码与 Shell 共享内存。每次调用模块都在 `catch_unwind` 下进行(`contain`、`contain_outside`):panic 会把该模块标记为失败,把它进行中的工具调用答复为“结果未知”,关闭它的额外窗口并显示 Restart 界面,而 Shell 继续运行([#114](https://github.com/OctoSense-org/OctoSense/pull/114),ADR 0004 步骤 9)。**已在 main。** + +### 脚本应用 + +脚本应用(系统应用 News、Photos、Maps、仅手机上的 Camera、Mail、AI providers 和 YouTube,来自 `desktop/system-apps.json` 和 `phone/system-apps.json`,以及商店应用)是 OctoScript 应用包。它们都在进程内、在 App Hub 的 **Card runner**(`octosense-app-hub-app` 的 `CARD_MODULE`)中运行:每个应用实例一个嵌套隔离环境,去掉了 `mod.res` 和 `mod.run`,有 jail 和配额。应用只能通过 `host.request(".", …)` 调用其清单获授权的服务族来访问 Shell。脚本的错误只会在它自己的隔离环境中失败。**已在 main。** + +```mermaid +flowchart TB + subgraph desktop["桌面端(macOS)"] + ds["OctoSense 进程
Shell + App Hub + Card runner + Rinx"] + dk["octos 子进程
OCTOS_APP_CORE_BIN serve --stdio"] + dt["Terminal 子进程
--stdin-loop"] + ds -- "stdin/stdout" --> dk + dt -- "hub WebSocket、IOSurface 画面" --> ds + end + subgraph android["Android(Home)"] + as["Home 进程
Shell + App Hub + Card runner + Rinx(全部进程内)"] + ak["liboctos.so 子进程
serve --stdio"] + as -- "stdin/stdout" --> ak + end + subgraph ohos["OpenHarmony"] + os["Home 进程
Shell + 内嵌 octos 任务"] + end + subgraph ios["iOS"] + is["Home 进程
无内核"] + end +``` + +## 2. Agent + +**每个 Shell 一个内核;Agent 是会话,不是进程。** 在这一个内核中,每个 Agent 都是一个以 tokio 任务运行的 octos 会话。文件编辑之类的工具在内核中运行;octos 的命令类工具在会话的工作区中以短时子进程运行(OctoSense 不让系统 Agent 使用它们,见[第 4 节](#4-工具与授权))。 + +| Agent | 是什么 | 状态 | +| --- | --- | --- | +| **系统 Agent** | `_main` profile 上的会话 `_main:api:octosense#system`(`crates/kernel/src/network.rs` 中的 `SYSTEM_SESSION`)。它拥有并监督所有应用 peer。目前用户通过 Talk to Octos 客户端与它对话;Shell 还没有为它绘制对话界面 | 已在 main | +| **应用 Agent** | 每个(应用,账号)一个由宿主拥有的 octos **peer**,归系统 Agent 所有(octos UPCR-2026-034,Rinx [ADR 0007](https://github.com/hagency-org/Rinx/blob/main/docs/adr/0007-host-owned-octos-app-peers.md)) | Rinx(原生)已在 main;脚本应用在开关后可用;见下文 | + +每个应用 peer 都独立拥有: + +- **工作区**:peer 会话所绑定的、由内核分配的目录。ADR 0004 §11 规定它是应用的账号目录(`apps//accounts//`);broker 目前还没有把这个目录传给内核(规划中,步骤 10;见[不一致之处](#代码与-adr-不一致之处)); +- **记忆命名空间** `app//acct-`(`crates/app-peers/src/broker.rs` 中的 `app_namespace()`);不返回它的内核会被拒绝; +- 自己的**对话记录**和**模型**通道(宿主通过 `peer/prepare` / `peer/model/set` 设置模型); +- 自己的**工具列表**:目前是内核对 peer 安全的默认工具;应用自己的工具和授权要等宿主注册工具(octos [#2567](https://github.com/octos-org/octos/pull/2567),未合并;OctoSense 计划步骤 6); +- **请求上下文**(`peer/context/open`):每个客户端实例一个(一个 Rinx 小程序、一个卡片会话),各有自己的对话记录、peer 工作区内的目录 `contexts//` 和子记忆命名空间。上下文读不到旁边的文件。 + +目前谁有 peer(`crates/ai-host/src/lib.rs`,`Policy::shipped()`;`crates/app-peers/src/hosted.rs`,`effective_services` = 声明 ∩ 支持 ∩ 策略): + +- **Rinx**,唯一获授权使用助手的原生应用(四个 `octos.*` 服务),需首次使用时的同意。 +- 声明了 `octos.*` 的**脚本应用**:每个应用一个 peer `card.`,账号为 `device`(`crates/ai-host/src/contained.rs`,[#106](https://github.com/OctoSense-org/OctoSense/pull/106)),前提是 `Policy::contained_apps` 开启(发布策略中默认关闭;`OCTOSENSE_CONTAINED_APPS=1` 可开启)且用户在首次使用时同意。 +- **AppCard**(可选)使用自己的内核连接和会话,而不是 peer。 +- 进程应用目前没有 Agent:只有 `ModuleHost::create` 中才会提供服务(peer link 规划中,步骤 8)。 + +peer 会被保留而不是丢弃:broker 用其宿主 token(存放在 `/../app-peers`,0600)恢复 peer,登出时也从不调用 `peer_close`,因为 octos 无法恢复已关闭的 peer,也无法为同一个(应用,账号)创建替代者(ADR 0004 §11)。 + +### 内核崩溃意味着什么 + +所有 Agent 都在同一个内核中,因此内核崩溃会同时停止**全部** Agent:系统 Agent 和每个应用 peer,包括正在进行的回合。Shell 和应用继续运行;`availability()` 报告助手失败,应用的常规界面照常工作,进行中的回合随连接一起结束。octos 已持久化的内容不会丢失:会话、黑板、记忆命名空间和 peer 绑定都在内核的数据目录中,因此下一次 `connect()` 启动新一代内核,使用方用保存的宿主 token 重新打开并恢复各自的 peer 和上下文。结果未知的工具调用未经用户同意绝不重试(ADR 0004 §7)。 + +## 3. 通信 + +```mermaid +flowchart LR + subgraph shell["Shell(宿主连接)"] + broker["app-peers broker"] + relay["工具/审批中继
(接口已留,等待 octos#2567)"] + end + subgraph kernel["octos 内核"] + sys["系统 Agent"] + p1["Calendar peer"] + p2["Mail peer"] + bb[("黑板
peers/<slug>/result.md、turns.txt")] + end + rinx["Rinx(模块)"] -- "OctosAppService" --> broker + card["脚本应用"] -- "host.request(octos.*)" --> broker + proc["进程应用"] -. "peer link(规划中)" .-> broker + broker -- "OUP:peer/prepare、peer/context/open、turn/start" --> kernel + sys -- "peer_send_input" --> p1 + p1 -- "写入" --> bb + sys -- "peer_gather / peer_list" --> bb + kernel -. "peer/input、peer/tool/call(octos#2567)" .-> relay +``` + +### 内核与客户端之间的 OUP + +内核使用 **octos UI 协议**(OUP;`octos-ui/v1alpha1`,JSON-RPC 2.0 帧;octos [`api/OCTOS_UI_PROTOCOL_V1_SPEC_2026-04-24.md`](https://github.com/octos-org/octos/blob/main/api/OCTOS_UI_PROTOCOL_V1_SPEC_2026-04-24.md))。**已在 main。** + +- **默认:stdio。** Shell 是唯一的客户端,经子进程的 stdin 和 stdout 通信(按行分隔的 JSON;octos UPCR-2026-016)。在 Shell 内部,`crates/kernel/src/router.rs` 在各原生使用方之间复用同一个帧流:每个请求获得内核范围内唯一的 id,其回复只发回该使用方;通知发给声明了该会话的使用方。 +- **Talk to Octos:宿主管理的 WebSocket**([ADR 0003](adr/0003-shared-octos-client-access.md),[#98](https://github.com/OctoSense-org/OctoSense/pull/98);octos [`docs/HOST_MANAGED_SERVE.md`](https://github.com/octos-org/octos/blob/main/docs/HOST_MANAGED_SERVE.md),UPCR-2026-036)。用户在 AI providers 中开启后(`/external-access.json`),Shell 以 `octos serve --host-managed --host 127.0.0.1` 重启内核,使用 Shell 只绑定一次并传下去的监听套接字(Unix 上为 `--listen-fd`)。两个 token 写在内核 stdin 的前两行,从不放入环境变量: + + | Token | 持有者 | 可以做什么 | + | --- | --- | --- | + | **宿主 token** | 只有 Shell 进程;每个 Shell 生命周期生成一次,从不记录日志 | 一切(管理员);原生使用方用它连接 `/api/ui-protocol/ws` | + | **外部 token** | 已配对的网页客户端(可信面板上的 8 位配对码,5 分钟有效,只能领取一次)或本用户的终端客户端(0600 的 `client-connection.json`) | 只能访问 `/api/ui-protocol/ws`,身份为 `_main`,且只能调用允许列表中的方法:打开并读取系统对话,启动、引导或中断**自己的**回合,回答自己回合的审批(一次性)和问题 | + + 外部客户端不能:调用任何 `peer/*` 方法或指定应用 peer 的会话(`peer-…`、`peerctx-…`);设置 `cwd`、`topic` 或 `sandbox`;触及提供方、密钥、模型、skills、快照或 `server/shutdown`;使用 REST 或管理路由。它们的回合只有固定的工具集(文件工具、`web_search`、`web_fetch`、记忆读取、`ask_user_question`、查看媒体;`crates/kernel/src/system_tools.rs` 中的 `EXTERNAL_TURN_TOOLS` 与之对应):没有 shell、没有 peer 工具、没有宿主路由的工具(octos [#2601](https://github.com/octos-org/octos/pull/2601))。OpenHarmony 和 iOS 上没有;Android 未验证。 + +### 内核中的系统 Agent 与应用 Agent + +使用 octos 的 peer 机制,深度为 1(peer 不能创建、引导或关闭 peer)。octos 中**已在 main**;这些工具位于 octos `crates/octos-agent/src/tools/`。 + +- **系统 Agent → 应用 Agent。** `peer_send_input`(只有发起者可用,最多 64 KB)经 peer 的收件箱把文字作为 peer 的下一个用户回合送达(在 serve 中是持久队列,每隔几秒处理一次,至少送达一次)。 +- **应用 Agent → 系统 Agent:黑板。** peer 的每个回合写入 `peers//result.md`(以及 `result-.md`),并在 `turns.txt` 中追加一行;系统 Agent 用 `peer_gather` 和 `peer_list` 读取(`awaiting_input` 表示 peer 正在等待回答问题)。这是 peer 之间唯一的通道。 +- **提问。** peer 用 `ask_user_question` 提问;系统 Agent 用 `peer_respond` 回答。`peer_respond` 从不回答审批(octos 会拒绝)。宿主驱动的回合没有 `ask_user_question`,因此 ADR 0004 §6 增加了 Shell 工具 **`host.ask`**,由 Shell 转给系统 Agent 或应用自己的对话。**规划中**(步骤 6)。 + +### 宿主拥有的路径:`peer/input` + +对宿主拥有的 peer,普通的 `peer_send_input` 会作为内核续跑执行,没有应用的工具或记忆。ADR 0004 §6 解决了这个问题:octos 把系统 Agent 的输入以 **`peer/input {peer, session_id, input_id, turn_id, text}`** 的形式送到宿主的驱动连接,由 Shell 自己启动回合(用内核的 `turn_id` 调用 `turn/start`),这样回合就带有应用的工具、记忆和上下文,其审批也出现在应用中。如果没有宿主连接持有该 peer,octos 会告诉系统 Agent 该应用未连接。**进行中**:内核一侧是 octos [#2567](https://github.com/octos-org/octos/pull/2567)(未合并,草稿;不在 octos `main` 上);Shell 一侧是计划步骤 6,OctoSense `main` 上还没有(不存在 `peer/input` 处理代码)。 + +### 应用与它自己的 Agent + +| 托管方式 | 通道 | 状态 | +| --- | --- | --- | +| 进程内原生模块(Rinx) | **注入的服务**:`create` 之前调用 `ai_host::offer`,在其中调用 `octosense_app_peers::injection::claim`,得到受限的 `OctosAppService`(`Open`、`History`、`Turn`、`Interrupt`、`Approval`);模块永远看不到协议 | 已在 main | +| 脚本应用 | 向 `octos` 宿主服务(`crates/ai-host/src/contained.rs`)调用 `host.request("octos.session.open" / "octos.session.history" / "octos.turn.start" / "octos.turn.interrupt", …)`;受清单、`Policy::contained_apps` 和首次使用同意(`consent_for_contained`)约束;其 peer 发起的工具审批一律被拒绝,并列在 `denied_approvals` 中 | 已在 main([#106](https://github.com/OctoSense-org/OctoSense/pull/106),同意机制来自 [#120](https://github.com/OctoSense-org/OctoSense/pull/120)) | +| 独立进程的原生应用 | **peer link**:应用 hub 连接上的独立通道(`PeerRequest`、`PeerReply`,以及由 Shell 盖上身份和调用方的 `PeerToolCall`),从不注册到 AI 总线;客户端 API 在 Makepad 的 `makepad-ai-services` 中 | 规划中(ADR 0004 §5,步骤 8);`hub.rs` 中尚无代码 | + +### Makepad 的 AI 服务总线与 OctoSense 的应用 Agent + +两种模型并存(ADR 0004,“Two AI models”): + +| | Makepad AI 服务总线 | OctoSense 应用 Agent | +| --- | --- | --- | +| 位置 | 窗口管理器一侧在 `crates/shell/src/ai_bus.rs`;上游 `libs/ai/services` | `crates/ai-host`、`crates/app-peers`、octos 内核 | +| 形态 | **一个中心对话**(桌面端的 AI 面板,即 Makepad 的 `aichat`)调用应用以风险等级(`Read`、`Act`、`Destructive`)注册的类型化工具 | **每个应用一个 Agent**,拥有应用的完整上下文(工作区、记忆、历史、工具),由系统 Agent 监督 | +| 路由 | Shell 给每个上行帧盖上发送方的端点,把注册转给面板(面板重连时重放),把面板的调用路由到应用的套接字,并自己回答 `os` 服务(list、launch、focus、close、open) | broker 直接用 OUP 与内核通信 | +| 用途 | 桌面端的 AI 面板;目前 Rinx 的助手工具;Terminal 的 `run`。其 `confirm: host` 调用经过 Shell 的审批路由([#120](https://github.com/OctoSense-org/OctoSense/pull/120)) | 应用自己的助手;系统 Agent 的委派 | + +**为什么应用 Agent 的流量不走总线。** 总线是通向一个掌握全部上下文的中心 Agent 的窄而单向的 API;应用 Agent 需要应用的完整上下文和一个私有、受监督的会话,而系统 Agent 必须通过内核的 peer 机制与它通信。总线也不携带账号、请求上下文或调用方,无法按 ADR 0004 §5 的要求在每次工具调用上盖上身份,而且其注册对面板可见。因此在 OctoSense 中,总线不是系统 Agent 通向应用的通道;它留给上游 Makepad 应用(ADR 0004 §6),进程应用的 peer link 也有意设计为独立通道。`crates/ai-host` 和 `crates/app-peers` 都不使用总线。 + +## 4. 工具与授权 + +**清单声明,用户在安装时授权,Shell 在每次调用时执行检查**(ADR 0004 §12,ADR 0002 §4)。脚本应用在 `manifest.json` 中声明能力,在 `tools.json` 中声明工具(由 App Hub 准入并锁定);原生应用在经过评审的 `native-apps.json` 条目中声明(`agent.octos`、`agent.tools`、`agent.tool_policy`)。 + +Agent 的工具来源: + +| 来源 | 示例 | 在哪里运行 | 状态 | +| --- | --- | --- | --- | +| 应用自己的工具(`tools.json`:名称 `.`、schema、`risk`、`confirm: host` 或 `app`、`shareable`) | `mail.send`、`rinx.message.send` | 应用的宿主服务、模块或进程,由 Shell 调用 | 作为 Agent 工具还在规划中(步骤 6,需要 octos#2567)。目前 Rinx 的工具经总线到达 AI 面板 | +| 系统工具箱,按能力授予(`research`、`crawl`、`model`) | `toolbox.search`、`toolbox.web_read`、`toolbox.deep_crawl`、`workflow.run` | 宿主 | 进行中([#108](https://github.com/OctoSense-org/OctoSense/pull/108));`model` 尚未注册 | +| octos 的通用工具 | 限定在工作区内的文件读取、记忆、`web_search`、`deep_search` | octos | 作为 octos 对 peer 安全的默认工具已在 main;按应用设置的 `generic_tools` 列表规划中(步骤 6) | +| 其他应用可共享的工具,由 Shell 路由 | Calendar 的 Agent 调用 `mail.send` | 所属应用,经 Shell | 规划中(ADR 0004 §7,步骤 6) | +| 命令执行 | `terminal.run`(Terminal 的可共享工具:`confirm: host`、`auto_approvable: false`) | 宿主工具,在用户可见的终端中 | 作为宿主工具规划中(步骤 6 和 11);常量已存在(`COMMAND_EXECUTION_TOOL`) | + +**系统 Agent 的工具集**(`crates/kernel/src/system_tools.rs`,[#117](https://github.com/OctoSense-org/OctoSense/pull/117))。**已在 main**,部分生效: + +- 默认列表 `SYSTEM_AGENT_TOOLS`:监督(`peer_send_input`、`peer_gather`、`peer_list`、`peer_respond`、`peer_close`;不含 `peer_handoff`)、其工作区的文件工具、`ask_user_question` 和查看媒体、记忆、`web_search` / `web_fetch`、`tool_search`。授权在此基础上增加(`SystemAgentTools`:工具箱工具、其他应用的可共享工具、命令执行)。 +- **从不提供 octos 自己的 shell。** 每次启动内核前,`enforce` 写入 `_main` profile 的 `tool_policy`,拒绝 `group:runtime`(`shell`、`bash`、`exec_command`、`write_stdin`)。它只替换 OctoSense 自己写的策略,并拒绝用户自己的 octos 主目录。 +- **还不是精确列表。** octos 没有宿主可以为单个会话设置的工具列表,因此系统 Agent 仍会得到 octos 注册的其余所有工具;精确列表的测试已保留,但在 octos#2567 之前被忽略。 +- **从 Settings 开启命令执行**(默认关闭,每条命令实时批准)只是接口:`SystemAgentTools::grant_command_execution` 已存在;没有 Settings 开关设置它,而且在 octos#2567 之前无法注册宿主工具。 + +**外部客户端**只有 octos 的固定允许列表([第 3 节](#内核与客户端之间的-oup));宿主路由的工具、开发者授权和 `dev.run` 永远到不了它们。 + +## 5. 审批 + +**授权不等于审批。** 授权表示 Agent 可以*拥有*某个工具;审批表示*这一次*调用、带着这些确切参数,可以执行。只读和应用内操作类工具授权后即可运行;对外或破坏性的调用(发送、发布、分享、购买、删除、运行命令)需要用户实时批准或由常设规则批准。只有用户能批准;系统 Agent 从不批准,Agent 自己输出的文字也从不作为审批界面(ADR 0004 §8)。 + +**审批路由**(`crates/shell/src/approvals/router.rs`,[#120](https://github.com/OctoSense-org/OctoSense/pull/120))是 Shell 中唯一回答审批请求的地方,依据的是确切参数。**已在 main。** 对每个请求依次: + +```mermaid +flowchart TB + req["审批请求
(所属应用、工具、确切参数、调用方、上下文)"] + dev{"1. 开发者模式
覆盖该应用?"} + capp{"2. confirm: app?"} + always{"3. auto_approvable: false、
结果未知,
或外部客户端?"} + rule{"4. 有针对(所属应用,工具)
的常设规则匹配?"} + sheet["5. Shell 绘制的面板
(每个请求一个,或在系统对话中
合并为一个)"] + ok(["批准,已审计"]) + appsheet["所属应用自己的面板,
显示调用方
(等待,超时后明确拒绝)"] + person["用户"] + req --> dev + dev -- 是 --> ok + dev -- 否 --> capp + capp -- 是 --> appsheet --> person + capp -- 否 --> always + always -- 是 --> sheet + always -- 否 --> rule + rule -- 是 --> ok + rule -- 否 --> sheet + sheet --> person +``` + +1. **开发者模式**(`dev_hooks.rs`、`crates/shell/src/dev_mode.rs`,[#118](https://github.com/OctoSense-org/OctoSense/pull/118))批准它所覆盖应用的一切,包括 `auto_approvable: false` 和 `confirm: app`,但从不替外部客户端批准。只有用户能开启它:开发构建中用 `OCTOSENSE_DEV_MODE=all`(或应用列表),发布构建只能用 `--dev-grant-all`,或在桌面端的 Developer options 中输入确认短语;商店构建永远不能。开启时显示横幅,审计每次调用,不在开发者 profile 中时 8 小时后或重启时自动结束。`dev.run` 尚未注册(步骤 11)。 +2. **`confirm: app`** 工具交给所属应用自己的面板(应用通过 `register_app_confirm` 注册的 `AppConfirm`),并附上调用方;规则不回答它们。未注册面板的应用有 120 秒(`app_wait_s`),之后调用被明确拒绝。`main` 上目前还没有应用注册面板;Rinx 的发送面板仍在 Rinx 内部作答。 +3. **`auto_approvable: false`**、**结果未知**和**外部客户端**的调用总是交给用户。 +4. **常设规则**(`rules.rs`),以**(所属应用,工具)**为键,不论谁调用;可以对确切参数设条件(收件人在联系人中或在该会话中、无附件、由用户触发、次数或金额上限;调用缺少所需信息时条件不成立),有每日上限(工具规则默认 20 次),范围最宽的规则(“这个应用接下来一小时的所有请求”)最多 60 分钟(没有“一切、永久”的规则),还有一个“关闭所有规则”。Shell 目前还没有联系人来源,因此“收件人在联系人中”目前从不匹配。由收到的内容触发的运行会被跳过,除非规则明确包含。用户在 Settings → Assistant → Approvals(`settings_page.rs`)中或从面板上创建规则;系统 Agent 只能建议。 +5. 否则显示 **Shell 绘制的面板**(`sheet.rs`、`view.rs`):每一行显示所属应用、工具、确切参数,跨应用调用时还显示调用方应用;系统 Agent 同一请求的 `confirm: host` 审批可以合并到它对话中的一个面板。 + +每个决定都只向中继发送一次,并写入**审计**(`audit.rs`:`/logs/approvals-audit.jsonl` 中每个决定一行 JSON,仅所有者可读,记录参数的摘要而不是参数本身;开发者模式在 `logs/dev-audit.jsonl` 中另有完整审计);每个自动决定也会作为通知告知用户。规则存放在 `approvals/rules.json`,同意记录在 `approvals/consent.json`,都在 OctoSense 主目录下。 + +**首次使用同意**(`consent.rs`):应用第一次请求它的 Agent 时,Shell 显示该 Agent 可以读取和使用什么,以及模型在哪里运行;Settings 列出每个应用的 Agent 并提供关闭开关(ADR 0004 §4)。原生模块(`module_host.rs` 中在提供服务之前调用 `consent_for_module`)和脚本应用(`consent_for_contained`)**已在 main**。 + +**目前谁在向路由提交请求:只有 AI 服务总线。** 面板对 `native-apps.json` 中列出的 `confirm: host` 工具(目前是 Terminal 的 `run`)的调用会被挂起(`bus::`),直到路由作答。octos 一侧(`peer/tool/call` 和内核审批进入 `relay.rs`)是等待 octos#2567 的接口;`main` 上没有调用 `set_relay`。在此之前,应用上下文中发起的 octos 审批交给该应用:脚本应用的在 `contained.rs` 中被拒绝,Rinx 的由用户在 Rinx 中回答,开发者模式下由 `crates/app-peers/src/host_approvals.rs` 为其覆盖的应用作答(有审计)。 + +## 6. 存储与机密 + +所有应用使用同一种由宿主拥有的布局,由其清单(`storage` 块)声明,且只由 Shell 计算(`crates/shell/src/app_storage/`,[#115](https://github.com/OctoSense-org/OctoSense/pull/115),ADR 0004 §11)。作为原生模块在 `create` 时获得的 API **已在 main**(`octosense_app_peers::storage`,与助手服务的提供方式相同): + +``` +/apps// 应用的 jail(App Hub 的 jail 根目录;原生应用的沙箱根目录) + accounts// 每个账号一个(应用没有账号时为 "device"): + 该账号的数据 = 该账号 Agent 的工作区 + common/ 与账号无关的应用数据 + cache/ 可清除,不备份 +/secrets// 宿主拥有:token、密钥、密码、加密存储 +``` + +- **OctoSense 主目录**在手机上是平台的应用数据目录,否则为 `~/.octosense`(`OCTOSENSE_HOME` 可覆盖;`crates/shell/src/octosense/paths.rs`)。应用根目录可用 `OCTOSENSE_APP_DATA` 移动;机密根目录始终是 `/secrets`,两者从不重叠。目录权限为 0700,拒绝路径中的符号链接。 +- **账号 hash** 是对规范化账号 id 做带域分隔的 SHA-256 后取 128 位(`account_hash`)。 +- **按 ADR,Agent 的工作区就是账号目录。** 目前目录已存在,模块可以通过 `AppStorage::agent_workspace` 获得它,但 broker 还没有把它作为 peer 的 `cwd` 传给 `peer/prepare`:peer 在内核分配的工作区中工作(规划中,步骤 10)。 +- **机密**(`secrets.rs`,`AppStorage::secrets`):macOS 和 iOS 上用钥匙串(每个 profile、应用和键一项),其他平台是 `secrets//` 中每个键一个仅所有者可读(0600)的明文文件;测试、无界面运行和 `OCTOSENSE_SECRETS=file` 也使用文件存储。Windows、Linux 和 Android 的系统密钥库是待办项。机密从不放在 `apps/` 下。 +- **启动检查**(`check.rs`):任何 Agent 工作区都不能包含或通向 `secrets/`(工作区或 jail 本身是符号链接、指向机密的符号链接或硬链接、机密根目录位于其中)。被标记的工作区会被拒绝(`agent_workspace` 对该账号或所有账号返回 `Refused`),直到之后某次启动时检查通过;启动照常进行,不删除任何内容。 +- **登出**会暂停该账号的 Agent,而不是关闭它:`Storage::sign_out` 让工作区返回 `SignedOut`。目前这只是接口(Shell 还没有账号事件;模块通过 `OctosAppService::set_account` 绑定账号);对工具调用返回 `signed_out` 还在规划中(步骤 6)。 +- **内核自己的数据**是分开的:core 目录 `<数据目录>/octos-home/.octos`(桌面端为 `~/.octosense/octos-home/.octos`,`OCTOS_APP_CORE_DIR` 可覆盖;`crates/kernel/src/dirs.rs`),存放 `_main` profile、会话、黑板和记忆命名空间。提供方密钥归 `llm` 服务管理(macOS 钥匙串,其他平台为仅所有者可读的文件;见 [ai-services.zh-CN.md](ai-services.zh-CN.md#ai-providers-与-llm-宿主服务))。peer 的宿主 token 在 `/../app-peers`(0600)。 +- **脚本应用**保留 App Hub 的 jail 和配额;它们的机密由宿主服务和宿主面板保管(“机密归宿主所有”,AGENTS.md 规则 3)。在数据迁入这一布局之前,Rinx 仍使用自己的数据目录([hagency-org/Rinx#37](https://github.com/hagency-org/Rinx/issues/37));显式设置了 `OCTOSENSE_HOME` 时,Shell 把它指向 `/apps/rinx/data`(`RINX_DATA_DIR`,在 `crates/shell/src/octosense/paths.rs` 中设置;显式设置的 `RINX_DATA_DIR` 优先)。 + +## 7. 信任边界与隔离 + +``` + 用户 ── 宿主面板(密钥、PIN、审批)──┐ + v + +------------------------ Shell 进程(可信)-----------------------------+ + | 持有:宿主 token、peer 宿主 token、提供方密钥(经 llm)、机密 | + | 每次调用都检查:授权、同意、审批、审计 | + | +------------------+ +------------------------------------------+ | + | | 原生模块 | | Card runner:脚本应用各在隔离环境中 | | + | | 经评审,共享 | | (jail、配额、按授权 host.request) | | + | | 内存:受信任 | +------------------------------------------+ | + | +------------------+ | + +-------|------------------------------------------|---------------------+ + | hub(回环) | OUP,宿主 token + +-------v----------+ +--------v--------------------+ + | 进程应用 | | octos 内核 | + | (操作系统沙箱: | | 每个 peer 的工作区围栏 | + | 规划中) | | 外部客户端:允许列表 | + +------------------+ +-----------------------------+ +``` + +| 边界 | 由什么保证 | 状态 | +| --- | --- | --- | +| 脚本应用 ↔ Shell | App Hub 的嵌套隔离环境:没有 `mod.res` / `mod.run`,有 jail 和配额,只能对获授权的服务族使用 `host.request`;密码输入框失效;机密在宿主面板上输入 | 已在 main | +| 原生模块 ↔ Shell | 内存上没有边界:只接受经评审的第一方代码(`native-apps.json`);模块边界上的 **panic 隔离**(`catch_unwind`,[#114](https://github.com/OctoSense-org/OctoSense/pull/114));局限:展开过程中再次 panic、`panic = "abort"`、FFI | 已在 main | +| 进程应用 ↔ Shell | 独立的地址空间;依据清单的 `sandbox` 和 `storage` 生成的**操作系统沙箱**(macOS 沙箱配置、Linux Landlock 和 seccomp、Windows AppContainer) | 沙箱规划中(ADR 0004 §3,步骤 8);目前进程应用以用户权限运行 | +| 应用 ↔ 内核 | 应用从不使用内核协议,**永远看不到宿主 token**:模块得到受限的 `OctosAppService`,脚本应用得到宿主服务,进程应用(规划中)使用 peer link | 已在 main | +| peer ↔ peer | octos:独立的工作区(拒绝重叠的工作区)、记忆命名空间、对话记录;请求上下文被限定在 `contexts//` 内 | 已在 main(octos UPCR-2026-034) | +| Agent ↔ 机密 | 机密位于所有 jail 和工作区之外;启动检查 | 已在 main | +| 外部客户端 ↔ 内核 | 外部 token、方法和工具允许列表、`Host` 和 origin 检查、无法访问 peer | 已在 main([ADR 0003](adr/0003-shared-octos-client-access.md)) | + +**Shell 在每次调用时检查什么**(ADR 0004 §3):授权和同意;预算、速率限制和后台策略;每次工具调用的名称和参数是否符合声明的 `tools.json`;每个结果是否符合其 schema 和大小;自己的审计;崩溃清理。目前 main 上已有:同意;按精确名称授予 `octos.*` 服务(声明 ∩ 支持 ∩ 策略);脚本应用的参数规则和大小上限(文字最多 32 KiB,回复最多 2 MiB);总线调用的审批路由和审计。工具调用的 schema 检查和预算随宿主注册工具一起到来(步骤 6)。 + +**谁都无法检查的**:原生应用的代码在它自己的工具里做了什么,或它为什么发起一个回合。控制手段是评审,以及进程应用的沙箱。 + +## 8. 完整示例:用邮件发送会议邀请 + +“让系统 Agent 发一封会议邀请邮件。”这是 ADR 0004 中的 Calendar 与 Mail 示例。**其中几乎全部都在规划中**:`main` 上没有 Calendar 应用,Mail 还没有声明 `tools.json` 或 `octos.*`,内核一侧(`peer/input`、宿主注册的工具)是 octos#2567。状态一栏说明哪些部分已经存在。 + +```mermaid +sequenceDiagram + actor P as 用户 + participant S as 系统 Agent + participant SH as Shell(宿主连接) + participant C as Calendar Agent(peer) + participant R as 审批路由 + participant M as Mail(宿主服务) + P->>S: "邀请 Ana、Bo 和 Edward 周二下午 3 点开会" + S->>C: peer_send_input(任务说明) + Note over S,SH: octos 把 peer/input 送到宿主 + SH->>C: turn/start(应用的工具、记忆、上下文) + C->>SH: peer/tool/call calendar.create_event + SH-->>C: 结果 + C->>SH: peer/tool/call mail.send x3(调用方:Calendar) + SH->>SH: 授权检查:Calendar 是否获授权 mail.send? + SH->>R: 审批(Mail、mail.send、确切参数、调用方 Calendar) + alt 针对(Mail, mail.send)的常设规则匹配 + R-->>SH: 批准(通知并审计) + else 没有规则 + R->>P: 一个合并面板,列出三封邀请 + P-->>R: 批准 + end + SH->>M: 执行 mail.send x3 + M-->>SH: 结果 + SH-->>C: peer/tool/result + C->>C: octos 写入 peers/(slug)/result.md + S->>S: peer_gather 读取结果 + S->>P: "已预订周二下午 3 点;已向 3 人发送邀请" +``` + +| 步骤 | 发生什么 | 状态 | +| --- | --- | --- | +| 1 | 用户在系统对话 `_main:api:octosense#system` 中向系统 Agent 提出请求。 | 目前只能从已配对的 Talk to Octos 客户端访问;Shell 还没有自己的系统 Agent 对话界面(桌面端的 AI 面板是总线上的 Makepad `aichat`,不是系统 Agent) | +| 2 | 系统 Agent 做计划。有歧义就提问,不去猜(“两个 Edward?”);有限的读取可以直接调用获授权的工具。需要 Calendar 自己判断的工作用 `peer_send_input` 交给 Calendar 的 Agent。 | `peer_send_input` 已在 main;直接的跨应用授权规划中(步骤 6) | +| 3 | octos 把输入以 `peer/input` 送到 Shell;Shell 在 Calendar 的 peer 上用 `turn/start` 启动回合,使其带有 Calendar 的工具、记忆和账号上下文。如果 Calendar 的 peer 没有宿主连接,系统 Agent 会被告知该应用未连接。 | 进行中([octos#2567](https://github.com/octos-org/octos/pull/2567));Shell 一侧规划中(步骤 6) | +| 4 | Calendar 的 Agent 调用 `calendar.create_event`(自己的工具),并为每位受邀者调用一次 Mail 可共享的 `mail.send`。每次调用都以 `peer/tool/call` 到达 Shell;Shell 盖上调用方(Calendar 的 Agent)、账号和上下文。 | 规划中(ADR 0004 §5、§7;步骤 6) | +| 5 | Shell 检查 Calendar 的清单是否获授权 `mail.send`(脚本应用在安装时授权);不需要第二道 Agent 级别的同意。 | 规划中(步骤 6) | +| 6 | `mail.send` 是对外的 `confirm: host` 工具,由审批路由处理:开发者模式未开启;不是 `confirm: app`;不是 `auto_approvable: false`;如果有针对(Mail,`mail.send`)的常设规则(例如“发给我的联系人”),就由规则批准(通知并审计;联系人条件需要 Shell 目前还没有的联系人来源),否则一个合并的 Shell 面板列出每封邀请:所属应用 Mail、工具 `mail.send`、调用方应用 Calendar 以及确切参数。 | 路由、规则、面板和审计已在 main([#120](https://github.com/OctoSense-org/OctoSense/pull/120));octos#2567 合入后由内核的工具调用提交请求 | +| 7 | 批准后,Shell 把每次调用交给 Mail 的宿主服务,它用用户在 Mail 宿主面板上登录的账号发送(密码永远不会到达 Agent),并把结果以 `peer/tool/result` 返回内核。 | Mail 的宿主服务已在 main(`apps/mail/host-service`);把它的工具作为 Agent 工具还在规划中 | +| 8 | Calendar 的回合结束,octos 写入 `peers//result.md` 和 `turns.txt`;系统 Agent 用 `peer_gather` 读取。失败的邀请会被点名,结果未知的邀请未经用户同意绝不重试。 | 黑板已在 main(octos) | +| 9 | 系统 Agent 宣布“已预订周二下午 3 点;已向 3 人发送邀请”。Calendar 和 Mail 自己的界面会显示变化,因为它们的数据变了。 | 规划中 | + +## 代码与 ADR 不一致之处 + +撰写本文时(2026-09-28)发现;本次改动只涉及文档,这里都没有修复。 + +1. **Terminal 之外的进程应用。** ADR 0004 §2 说目前只有 Terminal 是进程应用,并且没有 Vulkan 的 Linux 上所有应用都在进程内运行。`native-apps.json` 把可选的 Sheets 和 Reference 在 macOS、Windows 和 Linux 上都声明为 `process`(普通的 `process`,不是 `process-if-vulkan`),因此 OpenGL 或 X11 的 Linux 构建会把它们作为进程运行,并经 CPU 回读画面。 +2. **崩溃进程应用的重启。** ADR 0004 §2 说它的磁贴会显示已关闭并提供重启。Shell 实际上移除客户端并发出 “App stopped” 通知;Restart 界面只用于进程内模块(`module_view.rs`)。 +3. **Agent 工作区 = 账号目录。** ADR 0004 §11 说它就是 `peer/prepare` 的 `cwd`。broker 不向 `peer/prepare` 发送 `cwd`,把 peer 绑定到内核分配的工作区;存储 API 的 `agent_workspace` 没有与之连接。账号目录名(`account_hash`,SHA-256)和记忆命名空间标签(`broker.rs` 中的 FNV-1a)也是对账号的两种不同 hash。 +4. **ADR 0003 的 “What the profile runs”** 说 OctoSense 既不配置工具集也不配置沙箱。自 [#117](https://github.com/OctoSense-org/OctoSense/pull/117) 起,Shell 每次启动前都向 `_main` profile 写入拒绝 `group:runtime` 的 `tool_policy`,因此宿主自己的回合也没有 octos shell。 +5. **过时的背景描述。** ADR 0004 的背景表格和根 README 的目录结构表仍提到原生 News、Maps 和 Photos 对比模块(`apps/*/native`);ADR 0004 步骤 1 已删除它们。[ai-services.zh-CN.md](ai-services.zh-CN.md) 的日期是 2026-09-27:其架构图显示 Card runner “没有 `octos.*` 服务”(#106 已合入),并说脚本应用“在首次使用同意落地之前”关闭(同意机制已在 #120 落地;`Policy::contained_apps` 开关默认仍关闭)。 +6. **“在 Settings 中开启”命令执行**(`crates/kernel/README.md`、`system_tools.rs` 文档):还没有 Settings 开关;`SystemAgentTools::grant_command_execution` 只是接口。 +7. **`host::processes_available()` 的测试**只检查 `wasm32`,而函数本身还排除了原生移动平台。 +8. **审批,ADR 0004 §8。** 每次应用工具调用都应通过 `peer/tool/call` 到达路由;目前只有 AI 总线向它提交请求(见上文)。没有应用注册自己的 `confirm: app` 面板,因此这类调用会等待后被拒绝。审计记录的是参数摘要而不是参数。发送队列和撤销窗口尚未实现。 +9. **存储,ADR 0004 §11。** 机密只在 macOS 和 iOS 上使用系统钥匙串(其他平台为 0600 明文文件)。启动检查拒绝通过链接或包含关系通向机密的工作区,而不是查找 `secrets/` 路径,并且不会中止启动。`spec.rs` 中 `storage.accounts` 默认为 `false`,而 `consent.rs` 把缺省值当作 `true`。 +10. **开发者模式,ADR 0004 §13。** `dev.run` 尚未注册;Settings 只能为所有应用开启(选定应用只能通过 `OCTOSENSE_DEV_MODE`);手机上没有开启手势;进程内模块仍会显示自己的确认面板。 + +## 源码位置 + +| 内容 | 位置 | +| --- | --- | +| 原生应用清单、生成器、生成的表 | [`native-apps.json`](../native-apps.json)、[`tools/native_apps.py`](../tools/native_apps.py)、[`crates/shell/src/native_apps.rs`](../crates/shell/src/native_apps.rs) | +| 托管决策、宿主服务 | [`crates/shell/src/apps.rs`](../crates/shell/src/apps.rs)(`AppRegistry::hosting`、`register_host_services`) | +| 进程应用、hub | [`crates/shell/src/clients.rs`](../crates/shell/src/clients.rs)、[`crates/shell/src/hub.rs`](../crates/shell/src/hub.rs)、[`crates/shell/src/host.rs`](../crates/shell/src/host.rs) | +| 进程内模块、panic 隔离 | [`crates/shell/src/module_host.rs`](../crates/shell/src/module_host.rs)、[`crates/shell/src/module_panic_tests.rs`](../crates/shell/src/module_panic_tests.rs) | +| AI 服务总线(Shell 一侧) | [`crates/shell/src/ai_bus.rs`](../crates/shell/src/ai_bus.rs) | +| 审批、同意、审计、Settings 页面 | [`crates/shell/src/approvals/`](../crates/shell/src/approvals/mod.rs) | +| 开发者模式 | [`crates/shell/src/dev_mode.rs`](../crates/shell/src/dev_mode.rs) | +| 应用存储、机密、启动检查 | [`crates/shell/src/app_storage/`](../crates/shell/src/app_storage/mod.rs) | +| 内核服务:启动、目录、Talk to Octos、帧路由、系统 Agent 工具 | [`crates/kernel/src/`](../crates/kernel/README.zh-CN.md)(`launch.rs`、`dirs.rs`、`network.rs`、`router.rs`、`system_tools.rs`) | +| Shell 的 AI 入口、策略、提供服务;脚本应用的 `octos` 服务 | [`crates/ai-host/src/lib.rs`](../crates/ai-host/src/lib.rs)、[`crates/ai-host/src/contained.rs`](../crates/ai-host/src/contained.rs) | +| 应用 peer:broker、托管启动、存储约定、注入 | [`crates/app-peers/src/`](../crates/app-peers/README.md)(`broker.rs`、`hosted.rs`、`storage.rs`、`injection.rs`、`host_approvals.rs`) | +| 各 Shell 的脚本系统应用 | [`desktop/system-apps.json`](../desktop/system-apps.json)、[`phone/system-apps.json`](../phone/system-apps.json) | +| octos:宿主管理的 serve、UPCR、peer 工具 | [`docs/HOST_MANAGED_SERVE.md`](https://github.com/octos-org/octos/blob/main/docs/HOST_MANAGED_SERVE.md)、[UPCR-2026-034](https://github.com/octos-org/octos/blob/main/docs/OCTOS_UI_PROTOCOL_CHANGE_REQUEST_UPCR_2026_034_HOST_APP_PEERS.md)、UPCR-2026-035([octos#2567](https://github.com/octos-org/octos/pull/2567),不在 `main` 上)、[UPCR-2026-036](https://github.com/octos-org/octos/blob/main/docs/OCTOS_UI_PROTOCOL_CHANGE_REQUEST_UPCR_2026_036_HOST_MANAGED_SERVE.md)、[`crates/octos-agent/src/tools/`](https://github.com/octos-org/octos/tree/main/crates/octos-agent/src/tools) |