diff --git a/README.md b/README.md index ff4d6cc6..deaea454 100644 --- a/README.md +++ b/README.md @@ -80,8 +80,8 @@ flowchart LR - **The shell** hosts the window manager, the native apps, the Card runner with the script apps, the system chat, the approval router and the host-tool relay. Its AI side is [`crates/ai-host`](crates/ai-host/README.md), with the [app-peers broker](crates/app-peers/README.md) that drives each app agent. -- **The octos kernel** ([`crates/kernel`](crates/kernel/README.md)) starts on its first connection and exits with the shell. On the desktop and Android it is a child process that speaks OUP over stdio; on OpenHarmony it runs inside the shell; iOS has none. The person picks models and enters keys in the **AI providers** app, on host sheets. Keys stay with the shell (in the macOS keychain, or in an owner-only file elsewhere) and never reach an app. -- **Process apps** run outside the shell in an OS sandbox (Seatbelt on macOS, Landlock and seccomp on Linux, none yet on Windows). Today that is only the Terminal on the desktop. It sends its frames over the shell's local hub and reaches its agent over the peer link on the same connection. +- **The octos kernel** ([`crates/kernel`](crates/kernel/README.md)) starts on its first connection and exits with the shell. On the desktop and Android it is a child process that speaks OUP over stdio; on OpenHarmony it runs inside the shell; iOS has none. The person picks models and enters keys in the **AI providers** app, on host sheets. Keys stay with the shell (in the macOS keychain, elsewhere in a file in the shell's own data) and never reach an app. +- **Process apps** run outside the shell in an OS sandbox (Seatbelt on macOS, Landlock and seccomp on Linux, none yet on Windows). Today those are the Terminal and Task (Task Manager, which has no agent), on a desktop built from a checkout; release packages don't ship their binaries yet, so there the Terminal runs inside the shell and Task is absent. The Terminal sends its frames over the shell's local hub and reaches its agent over the peer link on the same connection. - **External clients** (Talk to Octos, opt-in) can use the system conversation from a browser or a terminal with a limited token. They get no app agent, no `peer/*` method and no host tool. The opt-in AppCard prototype is the one exception to all this: it opens its own kernel connection instead of going through the broker. For the full picture with code paths, read [docs/architecture.md](docs/architecture.md); for the trust model and how to test the AI services locally, [docs/ai-services.md](docs/ai-services.md); for the decisions behind it, [ADR 0004](docs/adr/0004-native-apps-hosting-and-peers.md). @@ -91,13 +91,13 @@ The opt-in AppCard prototype is the one exception to all this: it opens its own **The system agent** is the octos session `_main:api:octosense#system`. The person talks to it in the system chat: F8 or the dock's Assistant icon on the desktop, the Assistant tile on the phone. It has two sets of tools: - **Its own kernel tools**, the fixed list in [`SYSTEM_AGENT_TOOLS`](crates/kernel/src/system_tools.rs): the `peer_*` tools to supervise app agents, plus files in its own workspace, memory, questions to the person and web search. octos's own shell tools are never on it. -- **Host tools from the shell**: `agents.list` and `agents.ask` to find app agents and ask the person to allow one, `agents.provision` and `agents.status` to run Mail's new-mail automation, `terminal.run` while Setup's Command execution switch is on, and the read tools native apps share with it (the table's last column). +- **Host tools from the shell**: `agents.list` and `agents.ask` to find app agents and ask the person to allow one, `agents.provision` and `agents.status` to run Mail's new-mail automation, `terminal.run` while Setup's Command execution switch is on and the Terminal runs as its own sandboxed process, and the read tools native apps share with it (the table's last column). The system agent can never approve a tool call; only the person can. **An app agent** exists once the person allows it, on a sheet shown once per app. When it runs depends on the kind of app: -- A **script app's** agent is prepared at shell startup with the app's tools registered, so the system agent's `peer_list` sees it even while the app is closed. +- A **script app's** agent is prepared as soon as it is allowed, and again at each startup, with the app's tools registered, so the system agent's `peer_list` sees it even while the app is closed. - A **native app's** agent belongs to the app's open window and is live only while the app is open; its memory and transcript persist between openings. Its tools run in that window too: with Notes closed, a call answers "Open Notes first". Signing out keeps an agent. Removing the account or uninstalling the app erases its transcripts and memory. @@ -107,9 +107,9 @@ These apps have an agent: | App | Kind | Its agent's own tools | What the system agent may call | | --- | --- | --- | --- | | Rinx (Matrix chat) | native, in the shell | octos's file, memory and web tools | – | -| Terminal (desktop) | native, its own process | `terminal.read_screen`, `terminal.read_scrollback` | `terminal.run`, behind Setup's switch, approved per command | +| Terminal (desktop) | native; its own process in a checkout build, inside the shell in a release package | `terminal.read_screen`, `terminal.read_scrollback` | `terminal.run`, only while it runs as its own sandboxed process, behind Setup's switch, approved per command | | Calculator, Clock, Notes, Reminders, Weather | native, in the shell | each app's read tools | the same read tools | -| Mail | script app | `mail.*` tools scoped to the signed-in account, including `mail.publish_card` | – | +| Mail | script app | `mail.*` tools scoped to the signed-in account: reads, cards (`mail.publish_card`), and reply drafts it can propose but never send | – | | Calendar | script app | `calendar.events`, `calendar.add_event`, `calendar.remove_event` (asks first), `calendar.notify`, `calendar.agenda` | – | | News | script app | `news.list`, `news.read`, `news.notify` | – | | Photos, Maps, YouTube; Camera on phones | script apps | `.notify` | – | @@ -160,8 +160,6 @@ The `.notify` tools fill a fixed card template ([`notice.card`](crates/shel Mail's agent can also start on its own. Once the person has signed in, allowed Mail's agent and asked the system agent to turn on new-mail processing (`agents.provision`), a host worker syncs the inbox and starts the agent for each new message. The agent reads the message with its scoped tools and decides whether to post a card. No other app has events yet; the [Mail event walkthrough](docs/mail-agent-events.md) has the details. -On Android, tapping a card notification opens its exact expanded card. A stale notification falls back to Glance. - ### One app agent, two lanes The system agent and the person talk to the same app agent, each in a lane of their own: @@ -206,8 +204,8 @@ The person can talk to any app's agent directly. These turns run in the person's | Where | How | | --- | --- | | **"Ask <app>"** panel | A shell panel for every app with an agent, opened from the bar's "Ask <app>" button or with Shift+F8. On the desktop it opens beside the system chat. The phone draws it full screen but has no touch control for it yet. | -| **In-card chat** | Open Chat in an agent-enabled card workspace, or type in a card that declares `sys.chat` ([below](#in-card-chat)). | -| **The app's own UI** | An app can open the person's lane itself ([next section](#how-an-app-uses-its-agent)). Rinx draws its own assistant UI; for the other apps, the "Ask <app>" panel is the way in. | +| **In-card chat** | Open a card from an app with an agent and switch to its Chat tab, or type in a card that declares `sys.chat` ([below](#in-card-chat)). | +| **The app's own UI** | An app can open the person's lane itself ([next section](#how-an-app-uses-its-agent)), though no shipped app does yet; for all of them the "Ask <app>" panel is the way in. | The panel asks for consent first and shows both lanes, each message with its speaker. Its Stop button ends only the person's own turn. [docs/architecture.md §2](docs/architecture.md#2-agents) covers the rest of its behavior. @@ -219,7 +217,7 @@ An app reaches its agent only through the shell, never through the raw kernel pr | --- | --- | --- | | Script app | `host.request("octos.session.open" / "octos.session.history" / "octos.turn.start" / "octos.turn.interrupt")`, limited to the names its manifest declares | store apps. The system apps declare none: the shell drives their agents. | | Native app, in the shell or as a process | Makepad's `OctosPeer` client over the peer link: open the link, then `serve_tools` to answer the agent's calls to the app's own tools. The same code works in either hosting. | Calculator, Clock, Notes, Reminders, Weather, Terminal | -| Native app with the injected service | `OctosAppService`: `open_conversation` for the person's lane, `open_context` for a private context | Rinx | +| Native app with the injected service | `OctosAppService`: `open_conversation` for the person's lane, `open_context` for a private context | Rinx, which uses only `open_context`, for its mini apps | For a script app, the smallest useful integration is two capabilities in the manifest: @@ -251,13 +249,13 @@ In a shell with a kernel, the first call asks the person to allow the app's agen An agent can only work with what its app hands it. A script app declares all of this in its bundle; a native app declares it in its `native-apps.json` entry. -- **A declaration.** The manifest's `agent` block names the kernel tools the agent may use (the system apps ask only for `ask_user_question`), the model features it needs (`tool_calling`) and, optionally, an `AGENT.md` with instructions. A native app's entry also says which of its tools its own agent may call (`own_tools`) and which the system agent may call (`system_tools`). +- **A declaration.** The manifest's `agent` block names the kernel tools the agent may use (the system apps ask only for `ask_user_question`), the model features it needs (`tool_calling`) and, optionally, an `AGENT.md` with instructions and skills, which the shell sends with every turn. A native app's entry also says which of its tools its own agent may call (`own_tools`) and which the system agent may call (`system_tools`). - **Tools.** `tools.json` describes each tool, named `.`: its input schema, its `risk` (`read`, `act` or `destructive`), who confirms it (`confirm: host` for a shell sheet, `app` for the app's own sheet) and whether other apps' agents may use it (`shareable`). - **Something to run the tools.** A declared tool needs an executor: the app's host service (Mail, Calendar, News), the shell's notice service (`.notify` for the other system apps) or a native app's open window. Store apps have no host service, and a tool marked `implemented_by: "app"` has no executor in the Card runner yet. So today a store app's agent can talk, ask questions and read its folder, but cannot act through tools of its own. -- **Data.** By default the agent works in its account's folder, `apps//accounts//` (a single `device` folder for an app without accounts), and reads it with the host's read-only `files.list`, `files.read` and `files.search` (on Unix). An app can declare `storage.agent_workspace: "none"` instead; Calculator, Clock, Notes, Reminders, Weather and the Terminal do, so their agents see only what their tools return. No agent sees another account's folder. +- **Data.** The agent works in its account's folder, `apps//accounts//` (a single `device` folder for an app without accounts), and reads it with the host's read-only `files.list`, `files.read` and `files.search` (on Unix). A script app can declare `storage.agent_workspace: "none"` to give its agent no folder, so it sees only what its tools return; a native app's agent gets its folder either way. No agent sees another account's folder. - **Memory.** Each agent has its own memory namespace, `app//acct-`, erased with the account. - **A way to reach the person.** With the `glance` permission, its tools can publish cards. -- **Events** (only Mail, for now). A `triggers.events` entry, a skill and `AGENT.md` let Mail's agent react to new mail without being asked; the shell sends the admitted instructions and skill text with each of those turns. +- **Events** (only Mail, for now). A `triggers.events` entry lets Mail's agent react to new mail without being asked, guided by its `AGENT.md` and a triage skill. The steps for adding a tool (manifest, `tools.json`, grant, handler, approval path) are in [AGENTS.md](AGENTS.md#architecture-documentation-and-code-walkthroughs), and the design is [ADR 0002](docs/adr/0002-event-driven-app-agents.md). @@ -301,20 +299,22 @@ sequenceDiagram - **The relay** ([`crates/shell/src/host_tools/`](crates/shell/src/host_tools/)) receives every `peer/tool/call`. It checks that this caller may use this tool, validates the arguments against the tool's schema and charges the caller's budget (by default 32 tool calls a turn and 1000 a day). Only then does it run the tool in the app that owns it: a native app's open window, a script app's host service, or a process app over its peer link. It checks the result against the schema too. -- **The approval router** ([`crates/shell/src/approvals/`](crates/shell/src/approvals/)) decides in a fixed order. Developer mode, which only the person can turn on, approves routed calls for the apps it covers. Mail sending still requires its separate host-owned review and physical approval; developer mode and standing rules cannot authorize it. A `confirm: app` tool goes to the app's own sheet. Calls that must always ask, such as a Terminal command, go straight to a sheet. Everything else may be decided by the person's standing rules, and otherwise a shell sheet shows the exact arguments. Every decision is written to an audit log. [The walkthrough](docs/architecture-walkthrough.md#approval-order) gives the full order. +- **The approval router** ([`crates/shell/src/approvals/`](crates/shell/src/approvals/)) decides in a fixed order. Developer mode, which only the person can turn on, approves routed calls for the apps it covers. A `confirm: app` tool goes to the app's own sheet. Calls that must always ask, such as a Terminal command, go straight to a sheet. Everything else may be decided by the person's standing rules, and otherwise a shell sheet shows the exact arguments. Every decision is written to an audit log. [architecture.md §5](docs/architecture.md#5-approvals) gives the full order. Sending mail is outside this order: the person always approves the exact message on a host-owned review, by touch on the phone, and developer mode cannot skip it. - **Deadlines.** An approval or question nobody answers in 10 minutes is denied, never approved. If the turn is still running 30 seconds later, the shell interrupts it so the next turn can start. ### Cards and questions -An app with the `glance` permission publishes cards as itself (`glance.publish`, `glance.withdraw`, `glance.list`); the shell takes the publisher from the caller, never from the arguments. A card runs under its app's own permissions, so a button pressed in a card is the app's own action, not an agent tool call. The desktop README describes [the glance panel](desktop/README.md#the-glance-panel) where cards appear. +An app with the `glance` permission publishes cards as itself (`glance.publish`, `glance.withdraw`, `glance.list`); the shell takes the publisher from the caller, never from the arguments. A card runs under its app's own permissions, so a button pressed in a card is the app's own action, not an agent tool call. The desktop README describes [the glance panel](desktop/README.md#the-glance-panel) where cards appear. On the phone, a card's notification opens that card's workspace, or the glance page if the card is gone. #### In-card chat -On the phone, a compact Glance summary expands into a resident full-screen workspace. An agent-enabled publisher gets **Card / Chat** even without an embedded `sys.chat`; the host binds the conversation to the original publication account and supplies bounded card data and local L0 state. The generated source stays unchanged. Local selections are context, not proof of a completed external action; chat uses only the app's existing tools and consent. Cards without an agent have no Chat tab. +A card can carry a conversation with its app's agent, which answers in the person's lane: -A Mail reply card published with a host-issued `draft_id` uses **Email / Chat**, sharing one saved draft. Requested chat edits save through a revision-bound capability, and final review reads the exact saved recipient, subject and body. Only trusted physical Android touch can approve sending, including in developer mode. Desktop/accessibility send approval remains deferred. See [Composed Mail cards](docs/mail-composable-cards.md) for the DeepSeek/MiniMax phone results, formatting failure/retry and remaining UX limits. +- **An opened card** becomes a workspace: full screen on the phone, a centred window on the desktop. If the publishing app has an agent, the workspace has **Card** and **Chat** tabs, even when the card declares no chat. The shell gives the agent the card's data and local state as context, bound to the account that published it, and the chat uses only the app's existing tools and consent. +- **Mail reply cards** have **Email** and **Chat** tabs over one saved draft. The agent can edit the draft and propose sending it, but only the person sends, by approving the exact message on a host-owned review with a physical touch. Developer mode cannot skip that review, and desktop approval is not built yet. [Composed Mail cards](docs/mail-composable-cards.md) has the details and the phone test results. +- **A card that declares `sys.chat(app, thread, fields)`** keeps its declared thread. -An explicit `sys.chat(app, thread, fields)` keeps its declared thread. The host owns the transcript in the app's account folder; only what the person typed is recorded as theirs. The older [`mail-request.card`](crates/shell/resources/glance/mail-request.card) under `OCTOSENSE_GLANCE_DEMO=mail` still answers with canned text. See [`crates/l0-chat`](crates/l0-chat/README.md) and [`glance_chat.rs`](crates/shell/src/glance_chat.rs). +The shell owns every transcript, kept in the app's account folder, and records as the person's only what the person typed. Model-written text is marked as AI-written and never runs as an action. See [`crates/l0-chat`](crates/l0-chat/README.md). #### Questions @@ -328,7 +328,7 @@ A phone runs the shell, the kernel, up to a dozen app agents and their apps at t - **One connection to the kernel.** The system chat and every app agent's broker share a single stream to the kernel. A small router ([`crates/kernel/src/router.rs`](crates/kernel/src/router.rs)) gives each request a unique id and sends each notification to the consumers of its session. - **Threads per service, not per turn.** Turns are Tokio tasks. In the shell, the kernel service runs on one two-worker Tokio runtime, each live agent's broker on a one-thread runtime of its own (so a slow agent cannot stall another agent or the UI), and the system chat on one thread that wakes the UI through Makepad's `SignalToUI` only when something changed. The octos child process (desktop and Android) uses Tokio's default runtime, one worker per CPU core. - **Started on demand, stopped when idle.** The kernel starts on its first connection: at startup if the person has already allowed a script app's agent, otherwise when an agent or the system chat first needs it. The system chat connects only while its pane is open or a turn is running. With Talk to Octos off, the kernel stops when its last connection closes. An app with no granted agent gets no broker and no peer. -- **Apps share the shell process.** Script apps are isolates in one Card runner and native apps are modules, so most apps cost no process of their own. A script error stays inside its isolate, and a native module's panic is caught at the module boundary. Only an app that needs an OS sandbox runs as a process. +- **Apps share the shell process.** Script apps are isolates in one Card runner and native apps are modules, so most apps cost no process of their own. A script error stays inside its isolate, and a native module's panic is caught at the module boundary. Only an app declared as its own process (the Terminal, for its OS sandbox, and Task, which has no module) runs as one, where its binary is available. - **Zero-copy frames for process apps.** The Terminal's frames reach the shell as shared GPU surfaces: IOSurface on macOS, D3D11 shared handles on Windows, DMA_BUF on Linux with Vulkan. Where only a CPU copy would work (Linux with OpenGL), every app runs inside the shell instead. ## Layout diff --git a/README.zh-CN.md b/README.zh-CN.md index f65cd1f1..a8cb6190 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -80,8 +80,8 @@ flowchart LR - **Shell** 承载窗口管理器、原生应用、运行脚本应用的 Card runner、系统对话、审批路由和宿主工具中转。它的 AI 部分是 [`crates/ai-host`](crates/ai-host/README.md),其中的 [app-peers 代理](crates/app-peers/README.md)负责驱动每个应用 Agent。 -- **octos 内核**([`crates/kernel`](crates/kernel/README.zh-CN.md))在第一个连接到来时启动,随 Shell 一起退出。桌面端和 Android 上,它是通过 stdio 讲 OUP 的子进程;OpenHarmony 上,它在 Shell 进程内运行;iOS 上没有内核。用户在 **AI providers** 应用中、在宿主面板上选择模型并输入密钥。密钥留在 Shell 一侧(macOS 上存入钥匙串,其他平台存入仅所有者可读的文件),永远不会到达应用。 -- **进程应用**在 Shell 之外运行,处在系统沙箱中(macOS 上是 Seatbelt,Linux 上是 Landlock 和 seccomp,Windows 上尚未实现)。目前只有桌面端的 Terminal 是进程应用。它通过 Shell 的本地 hub 发送画面,并通过同一连接上的 peer link 使用自己的 Agent。 +- **octos 内核**([`crates/kernel`](crates/kernel/README.zh-CN.md))在第一个连接到来时启动,随 Shell 一起退出。桌面端和 Android 上,它是通过 stdio 讲 OUP 的子进程;OpenHarmony 上,它在 Shell 进程内运行;iOS 上没有内核。用户在 **AI providers** 应用中、在宿主面板上选择模型并输入密钥。密钥留在 Shell 一侧(macOS 上存入钥匙串,其他平台存入 Shell 自己数据目录中的文件),永远不会到达应用。 +- **进程应用**在 Shell 之外运行,处在系统沙箱中(macOS 上是 Seatbelt,Linux 上是 Landlock 和 seccomp,Windows 上尚未实现)。目前的进程应用是 Terminal 和 Task(Task Manager,没有 Agent),而且只在从源码检出构建的桌面端上;发布包还不附带它们的二进制,所以发布包里 Terminal 运行在 Shell 内,也没有 Task。Terminal 通过 Shell 的本地 hub 发送画面,并通过同一连接上的 peer link 使用自己的 Agent。 - **外部客户端**(Talk to Octos,需手动开启)可以凭受限的 token 从浏览器或终端使用系统对话,但拿不到任何应用 Agent、`peer/*` 方法或宿主工具。 可选的 AppCard 原型是唯一的例外:它不经过代理,而是自己打开内核连接。想看带代码路径的完整说明,请读 [docs/architecture.zh-CN.md](docs/architecture.zh-CN.md);信任模型以及如何在本地测试 AI 服务,见 [docs/ai-services.zh-CN.md](docs/ai-services.zh-CN.md);背后的决策见 [ADR 0004(英文)](docs/adr/0004-native-apps-hosting-and-peers.md)。 @@ -91,13 +91,13 @@ flowchart LR **系统 Agent** 是 octos 会话 `_main:api:octosense#system`。用户在系统对话中与它交谈:桌面端按 F8 或点 Dock 上的 Assistant 图标,手机上点 Assistant 磁贴。它有两组工具: - **它自己的内核工具**,即 [`SYSTEM_AGENT_TOOLS`](crates/kernel/src/system_tools.rs) 中固定的列表:用于监督应用 Agent 的 `peer_*` 工具,以及自己工作区里的文件、记忆、向用户提问和网页搜索。octos 自带的 shell 工具永远不在其中。 -- **Shell 为它注册的宿主工具**:`agents.list` 和 `agents.ask` 用来查找应用 Agent、请用户允许某个 Agent;`agents.provision` 和 `agents.status` 用来运行邮件的新邮件自动处理;Setup 中的 Command execution 开关打开时还有 `terminal.run`;以及原生应用共享给它的只读工具(见下表最后一列)。 +- **Shell 为它注册的宿主工具**:`agents.list` 和 `agents.ask` 用来查找应用 Agent、请用户允许某个 Agent;`agents.provision` 和 `agents.status` 用来运行邮件的新邮件自动处理;Setup 中的 Command execution 开关打开、且 Terminal 作为独立的沙箱进程运行时,还有 `terminal.run`;以及原生应用共享给它的只读工具(见下表最后一列)。 系统 Agent 永远不能批准工具调用,只有用户可以。 **应用 Agent** 要等用户允许后才会存在,每个应用只问一次。它何时运行取决于应用的类型: -- **脚本应用**的 Agent 在 Shell 启动时就准备好,并注册了应用的工具,所以即使应用没有打开,系统 Agent 的 `peer_list` 也能看到它。 +- **脚本应用**的 Agent 在获准时立即准备好,此后每次启动时也会准备,并注册应用的工具,所以即使应用没有打开,系统 Agent 的 `peer_list` 也能看到它。 - **原生应用**的 Agent 属于应用已打开的窗口,只在应用打开期间运行;它的记忆和对话记录在两次打开之间会保留。它的工具也在那个窗口里运行:Notes 没打开时,调用会回答 “Open Notes first”。 退出登录会保留 Agent;删除账号或卸载应用会清除它的对话记录和记忆。 @@ -107,9 +107,9 @@ flowchart LR | 应用 | 类型 | 它的 Agent 自己的工具 | 系统 Agent 可以调用 | | --- | --- | --- | --- | | Rinx(Matrix 聊天) | 原生,在 Shell 内 | octos 的文件、记忆和网页工具 | – | -| Terminal(桌面端) | 原生,独立进程 | `terminal.read_screen`、`terminal.read_scrollback` | `terminal.run`,受 Setup 开关控制,每条命令都要批准 | +| Terminal(桌面端) | 原生;源码检出构建中为独立进程,发布包中在 Shell 内 | `terminal.read_screen`、`terminal.read_scrollback` | `terminal.run`,只在它作为独立的沙箱进程运行时提供,受 Setup 开关控制,每条命令都要批准 | | Calculator、Clock、Notes、Reminders、Weather | 原生,在 Shell 内 | 各自的只读工具 | 同样的只读工具 | -| 邮件 | 脚本应用 | 绑定当前登录账号的 `mail.*` 工具,包括 `mail.publish_card` | – | +| 邮件 | 脚本应用 | 绑定当前登录账号的 `mail.*` 工具:读取、卡片(`mail.publish_card`),以及它可以提议但永远不能发送的回复草稿 | – | | 日历 | 脚本应用 | `calendar.events`、`calendar.add_event`、`calendar.remove_event`(先问用户)、`calendar.notify`、`calendar.agenda` | – | | 新闻 | 脚本应用 | `news.list`、`news.read`、`news.notify` | – | | 相册、地图、YouTube;手机上的相机 | 脚本应用 | `.notify` | – | @@ -160,8 +160,6 @@ sequenceDiagram 邮件的 Agent 也可以自己启动。用户登录、允许邮件的 Agent,并请系统 Agent 打开新邮件处理(`agents.provision`)之后,宿主会在后台同步收件箱,每来一封新邮件就启动一次 Agent。Agent 用绑定账号的工具读取邮件,自己判断要不要发卡片。其他应用还没有事件;详见[邮件事件导读](docs/mail-agent-events.zh-CN.md)。 -在 Android 上点击卡片通知会打开准确对应的展开卡片;过期通知回退到 Glance。 - ### 一个应用 Agent,两条通道 系统 Agent 和用户与同一个应用 Agent 对话,但各走各的通道: @@ -206,8 +204,8 @@ flowchart TB | 入口 | 怎么用 | | --- | --- | | **“Ask <app>”** 面板 | Shell 为每个有 Agent 的应用提供的面板,从顶栏的 “Ask <app>” 按钮或按 Shift+F8 打开。桌面端上它出现在系统对话旁边。手机上它全屏显示,但还没有可以打开它的触控入口。 | -| **卡内对话** | 在有 Agent 的卡片工作区打开 Chat,或在显式声明 `sys.chat` 的卡片里输入([见下文](#卡内对话))。 | -| **应用自己的界面** | 应用可以自己打开用户的通道([见下一节](#应用如何使用自己的-agent))。Rinx 绘制自己的助手界面;其他应用通过 “Ask <app>” 面板进入。 | +| **卡内对话** | 打开有 Agent 的应用发布的卡片,切换到 Chat 标签页;或在声明了 `sys.chat` 的卡片里输入([见下文](#卡内对话))。 | +| **应用自己的界面** | 应用可以自己打开用户的通道([见下一节](#应用如何使用自己的-agent)),但随附的应用都还没有这样做;所有应用都通过 “Ask <app>” 面板进入。 | 面板会先征得同意,然后显示两条通道,每条消息都标明说话者。面板上的“停止”只结束用户自己的回合。面板的其他行为见 [docs/architecture.zh-CN.md 第 2 节](docs/architecture.zh-CN.md#2-agent)。 @@ -219,7 +217,7 @@ flowchart TB | --- | --- | --- | | 脚本应用 | `host.request("octos.session.open" / "octos.session.history" / "octos.turn.start" / "octos.turn.interrupt")`,限于 manifest 声明的名称 | 商店应用。系统应用都没有声明:它们的 Agent 由 Shell 驱动。 | | 原生应用(在 Shell 内或作为进程) | Makepad 的 `OctosPeer` 客户端,经由 peer link:先打开链接,再用 `serve_tools` 应答 Agent 对应用自身工具的调用。同一份代码在两种托管方式下都能用。 | Calculator、Clock、Notes、Reminders、Weather、Terminal | -| 使用注入服务的原生应用 | `OctosAppService`:`open_conversation` 打开用户的通道,`open_context` 打开私有上下文 | Rinx | +| 使用注入服务的原生应用 | `OctosAppService`:`open_conversation` 打开用户的通道,`open_context` 打开私有上下文 | Rinx,目前只用 `open_context`,供它的小程序使用 | 对脚本应用来说,最小可用的接入只需要在 manifest 中申请两个能力: @@ -251,13 +249,13 @@ fn ask(){ Agent 能做什么,取决于应用交给它什么。脚本应用把这些都声明在应用包里;原生应用则声明在 `native-apps.json` 的条目里。 -- **声明。** manifest 的 `agent` 块列出 Agent 可用的内核工具(系统应用只申请了 `ask_user_question`)、需要的模型能力(`tool_calling`),以及可选的、写有指令的 `AGENT.md`。原生应用的条目还会说明它自己的 Agent 可以调用它的哪些工具(`own_tools`),系统 Agent 又可以调用哪些(`system_tools`)。 +- **声明。** manifest 的 `agent` 块列出 Agent 可用的内核工具(系统应用只申请了 `ask_user_question`)、需要的模型能力(`tool_calling`),以及可选的、写有指令的 `AGENT.md` 和技能,Shell 会随每一轮发送它们。原生应用的条目还会说明它自己的 Agent 可以调用它的哪些工具(`own_tools`),系统 Agent 又可以调用哪些(`system_tools`)。 - **工具。** `tools.json` 描述每个工具(命名为 `.`):输入 schema、`risk`(`read`、`act` 或 `destructive`)、由谁确认(`confirm: host` 用 Shell 面板,`app` 用应用自己的面板),以及其他应用的 Agent 能否使用(`shareable`)。 - **执行工具的地方。** 声明了的工具还需要执行者:应用的宿主服务(邮件、日历、新闻)、Shell 的通知服务(其他系统应用的 `.notify`),或原生应用已打开的窗口。商店应用没有宿主服务,而标为 `implemented_by: "app"` 的工具在 Card runner 中还没有执行器。所以目前商店应用的 Agent 能对话、能提问、能读取自己的文件夹,但还不能通过自己的工具做事。 -- **数据。** 默认情况下,Agent 在它所属账号的文件夹 `apps//accounts//` 中工作(不区分账号的应用只有一个 `device` 文件夹),并用宿主的只读工具 `files.list`、`files.read` 和 `files.search`(Unix 上)读取它。应用也可以声明 `storage.agent_workspace: "none"`;Calculator、Clock、Notes、Reminders、Weather 和 Terminal 就是这样,它们的 Agent 只能看到自己的工具返回的内容。任何 Agent 都看不到别的账号的文件夹。 +- **数据。** Agent 在它所属账号的文件夹 `apps//accounts//` 中工作(不区分账号的应用只有一个 `device` 文件夹),并用宿主的只读工具 `files.list`、`files.read` 和 `files.search`(Unix 上)读取它。脚本应用可以声明 `storage.agent_workspace: "none"`,让 Agent 没有文件夹,只能看到自己的工具返回的内容;原生应用的 Agent 无论怎样声明都会得到自己的文件夹。任何 Agent 都看不到别的账号的文件夹。 - **记忆。** 每个 Agent 有自己的记忆命名空间 `app//acct-`,随账号一起清除。 - **联系用户的方式。** 有了 `glance` 权限,它的工具就能发布卡片。 -- **事件**(目前只有邮件)。一个 `triggers.events` 条目、一项技能和 `AGENT.md`,让邮件的 Agent 不等人开口就能处理新邮件;在这些回合里,Shell 会附上已接纳的指令和技能文本。 +- **事件**(目前只有邮件)。一个 `triggers.events` 条目让邮件的 Agent 不等人开口就能处理新邮件,并由它的 `AGENT.md` 和一项分拣技能指导。 新增一个工具的步骤(manifest、`tools.json`、授权、处理代码、审批路径)见 [AGENTS.md(英文)](AGENTS.md#architecture-documentation-and-code-walkthroughs),设计见 [ADR 0002(英文)](docs/adr/0002-event-driven-app-agents.md)。 @@ -301,20 +299,22 @@ sequenceDiagram - **中转**([`crates/shell/src/host_tools/`](crates/shell/src/host_tools/))接收每一个 `peer/tool/call`。它先检查这个调用方能否使用这个工具,按工具的 schema 校验参数,并从调用方的预算中扣除(默认每轮 32 次、每天 1000 次工具调用);之后才在拥有该工具的应用中运行它:原生应用已打开的窗口、脚本应用的宿主服务,或经 peer link 交给进程应用。结果同样要按 schema 检查。 -- **审批路由**([`crates/shell/src/approvals/`](crates/shell/src/approvals/))按固定顺序做决定。开发者模式只能由用户打开,它会批准所覆盖应用经路由处理的调用。邮件发送仍要求独立的宿主审核和实体批准;开发者模式和常设规则不能授权发送。`confirm: app` 的工具交给应用自己的面板。必须每次都问的调用(例如 Terminal 的命令)直接弹出面板。其余调用可以由用户的常设规则决定,否则由 Shell 面板展示确切参数,请用户确认。每个决定都会写入审计日志。完整顺序见[导读](docs/architecture-walkthrough.zh-CN.md#审批顺序)。 +- **审批路由**([`crates/shell/src/approvals/`](crates/shell/src/approvals/))按固定顺序做决定。开发者模式只能由用户打开,它会批准所覆盖应用经路由的调用。`confirm: app` 的工具交给应用自己的面板。必须每次都问的调用(例如 Terminal 的命令)直接弹出面板。其余调用可以由用户的常设规则决定,否则由 Shell 面板展示确切参数,请用户确认。每个决定都会写入审计日志。完整顺序见 [architecture.zh-CN.md 第 5 节](docs/architecture.zh-CN.md#5-审批)。发送邮件不走这个顺序:总是由用户在宿主自己的审阅界面上确认确切的邮件内容,在手机上需要亲手触摸,开发者模式也不能跳过。 - **时限。** 10 分钟内无人回答的审批或提问会被拒绝,绝不会被批准。如果 30 秒后这一轮仍在运行,Shell 会中断它,好让下一轮开始。 ### 卡片与提问 -拥有 `glance` 权限的应用以自己的身份发布卡片(`glance.publish`、`glance.withdraw`、`glance.list`);Shell 从调用方取得发布者,从不读取参数中的发布者。卡片在应用自己的权限下运行,所以在卡片上按下按钮是应用自己的操作,不是 Agent 的工具调用。卡片显示在桌面端的[一览面板](desktop/README.zh-CN.md#一览面板)中,桌面端 README 对它有详细说明。 +拥有 `glance` 权限的应用以自己的身份发布卡片(`glance.publish`、`glance.withdraw`、`glance.list`);Shell 从调用方取得发布者,从不读取参数中的发布者。卡片在应用自己的权限下运行,所以在卡片上按下按钮是应用自己的操作,不是 Agent 的工具调用。卡片显示在桌面端的[一览面板](desktop/README.zh-CN.md#一览面板)中,桌面端 README 对它有详细说明。在手机上,卡片的通知会打开那张卡片的工作区;卡片已不存在时打开 glance 页面。 #### 卡内对话 -在手机上,紧凑的 Glance 摘要展开为驻留的全屏工作区。有 Agent 的发布者即使没有内嵌 `sys.chat`,也会获得 **Card / Chat**;宿主把会话绑定到发布时的账户,并提供有大小限制的卡片数据和 L0 本地状态。生成源码保持不变。本地选项是上下文,不代表外部操作已经完成;聊天只使用应用已有的工具和同意权限。没有 Agent 的卡片不显示 Chat 页签。 +卡片可以带着与其应用 Agent 的对话,Agent 在用户的通道里回答: -携带宿主签发 `draft_id` 的 Mail 回复卡片使用 **Email / Chat**,共用一份已保存草稿。用户要求的聊天修改通过绑定修订号的凭证保存,最终审核读取确切的已保存收件人、主题和正文。即使在开发者模式,发送仍只能由可信 Android 实体触摸批准;桌面/无障碍发送审批暂缓。DeepSeek/MiniMax 实机结果、格式失败及重试、剩余 UX 限制见[组合 Mail 卡片](docs/mail-composable-cards.zh-CN.md)。 +- **打开的卡片**会成为一个工作区:手机上是全屏,桌面端是居中的窗口。如果发布卡片的应用有 Agent,工作区就有 **Card** 和 **Chat** 两个标签页,即使卡片没有声明对话也是如此。Shell 把卡片的数据和本地状态作为上下文交给 Agent,并绑定到发布卡片的账号;对话只使用应用已有的工具和已有的同意。 +- **邮件回复卡片**有 **Email** 和 **Chat** 两个标签页,共用一份保存的草稿。Agent 可以修改草稿并提议发送,但只有用户能发送:用户要在宿主自己的审阅界面上亲手触摸,确认确切的邮件内容。开发者模式不能跳过这一步,桌面端的确认还没有实现。详情和手机上的测试结果见[可组合的邮件卡片](docs/mail-composable-cards.zh-CN.md)。 +- **声明了 `sys.chat(app, thread, fields)` 的卡片**保留它声明的对话线程。 -显式 `sys.chat(app, thread, fields)` 保留声明的线程。对话记录由宿主保存在应用账户文件夹中,只有用户亲手输入的内容才记为用户的话。旧的 [`mail-request.card`](crates/shell/resources/glance/mail-request.card) 在 `OCTOSENSE_GLANCE_DEMO=mail` 下仍使用固定回答。详见 [`crates/l0-chat`](crates/l0-chat/README.zh-CN.md) 和 [`glance_chat.rs`](crates/shell/src/glance_chat.rs)。 +所有对话记录都归 Shell 所有,保存在应用的账号文件夹中;只有用户亲手输入的内容才记为用户的话。模型写的文字会标为 AI 撰写,且从不作为操作执行。详见 [`crates/l0-chat`](crates/l0-chat/README.md)。 #### 提问 @@ -328,7 +328,7 @@ Agent 的 `ask_user_question` 出现在这一轮的来处:用户和应用自 - **到内核只有一条连接。** 系统对话和每个应用 Agent 的代理共用一条通往内核的流。一个小小的路由器([`crates/kernel/src/router.rs`](crates/kernel/src/router.rs))为每个请求分配唯一的 id,并把每条通知只发给关心该会话的使用方。 - **线程按服务分配,而不是按回合。** 回合是 Tokio 任务。在 Shell 中,内核服务运行在一个有两个工作线程的 Tokio 运行时上,每个活跃应用 Agent 的代理有自己的单线程运行时(所以一个慢吞吞的 Agent 拖不住别的 Agent,也拖不住界面),系统对话则用一个线程,只在有变化时才通过 Makepad 的 `SignalToUI` 唤醒界面。octos 子进程(桌面端和 Android)使用 Tokio 的默认运行时,每个 CPU 核心一个工作线程。 - **按需启动,空闲即停。** 内核在第一个连接到来时启动:如果用户已经允许过某个脚本应用的 Agent,就在 Shell 启动时;否则在某个 Agent 或系统对话第一次需要它时。系统对话只在面板打开或有回合运行时才保持连接。Talk to Octos 关闭时,最后一个连接断开,内核就停止。没有获准 Agent 的应用既没有代理,也没有 peer。 -- **应用共享 Shell 进程。** 脚本应用是同一个 Card runner 中的隔离环境,原生应用是模块,所以大多数应用不单独占用进程。脚本出错只影响它自己的隔离环境,原生模块的 panic 在模块边界被捕获。只有需要系统沙箱的应用才作为独立进程运行。 +- **应用共享 Shell 进程。** 脚本应用是同一个 Card runner 中的隔离环境,原生应用是模块,所以大多数应用不单独占用进程。脚本出错只影响它自己的隔离环境,原生模块的 panic 在模块边界被捕获。只有声明为独立进程的应用(为了系统沙箱的 Terminal,以及没有模块的 Task)才作为进程运行,前提是有它的二进制。 - **进程应用的画面零拷贝。** Terminal 的画面以共享 GPU 表面的形式到达 Shell:macOS 上是 IOSurface,Windows 上是 D3D11 共享句柄,Linux 配 Vulkan 时是 DMA_BUF。在只能靠 CPU 拷贝的环境(Linux 配 OpenGL)中,所有应用都改在 Shell 内运行。 ## 目录结构 diff --git a/docs/adr/0002-event-driven-app-agents.md b/docs/adr/0002-event-driven-app-agents.md index afdf924c..f3299a26 100644 --- a/docs/adr/0002-event-driven-app-agents.md +++ b/docs/adr/0002-event-driven-app-agents.md @@ -1,7 +1,7 @@ # ADR 0002: Event-driven app agents: apps think on their own triggers and publish cards to the glance screen - **Date:** 2026-09-27 -- **Status:** Proposed; amended 2026-09-28 by [ADR 0004](0004-native-apps-hosting-and-peers.md) (sections 1, 4, 6, 10, 12 and 13; see the amendment at the end); notes dated 2026-09-29 record what has since changed; amended 2026-10-03 by [ADR 0006](0006-app-studio-on-the-phone.md) (sections 6 and 7; see the amendment at the end) +- **Status:** Proposed; partly implemented (2026-10-04: §6's toolbox, §7's `sys.digest` and `glance.publish`, §14's `model.complete`, News M1 and the Mail incoming-event slice are on `main`; see the implementation amendment of 2026-10-03); amended 2026-09-28 by [ADR 0004](0004-native-apps-hosting-and-peers.md) (sections 1, 4, 6, 10, 12 and 13; see the amendment at the end); notes dated 2026-09-29 record what has since changed; amended 2026-10-03 by [ADR 0006](0006-app-studio-on-the-phone.md) (sections 6 and 7; see the amendment at the end) - **Scope:** How the assistant works in OctoSense when no person is typing, for any app: which agent runs, what starts it, which tools and data it may use, how it gathers information, how it produces and checks a card, and where the card and what it learned go. - **Relates to:** [ADR 0001](0001-one-octosense-repository.md) (one repository; [`crates/kernel`](../../crates/kernel), [`crates/app-peers`](../../crates/app-peers), [`crates/ai-host`](../../crates/ai-host), [`crates/shell`](../../crates/shell)); [Home ADR 0002](home/0002-agentic-app-security-model.md) (agentic app security model), [Home ADR 0003](home/0003-app-hub-and-store.md) (App Hub), [Home ADR 0004](home/0004-system-apps-are-contained-script-apps.md) (system apps are contained script apps); [Rinx ADR 0007](https://github.com/hagency-org/Rinx/blob/main/docs/adr/0007-host-owned-octos-app-peers.md) (host-owned octos app peers); octos ADR "personal memory tiers" (octos-org/octos#2365); OctoScript [`docs/ui-profile-l0.md`](https://github.com/OctoSense-org/OctoScript/blob/main/docs/ui-profile-l0.md) (the L0/L1/L2 card levels). @@ -244,7 +244,7 @@ Least privilege, declared by the app, checked by App Hub, granted by the host, e | Layer | Restriction | Enforced by | |---|---|---| -| **Tools** | only the app's own `tools.json` plus the generic tools its manifest names (`agent.tools`) and the person granted at install, with no fixed exclusions; tools of other apps only where the manifest asks, the shell granted them and the owning app marks them shareable, registered by the shell and marked with the owning app; nothing else is visible to the model | kernel, per peer context (script apps and native modules alike); the shell checks every cross-app call against the grants | +| **Tools** | only the app's own `tools.json` plus the generic tools its manifest names (`agent.tools`) and the person granted at install, with no fixed exclusions *(2026-10-04: narrower in what ships; see ADR 0004 §12's note)*; tools of other apps only where the manifest asks, the shell granted them and the owning app marks them shareable, registered by the shell and marked with the owning app; nothing else is visible to the model | kernel, per peer context (script apps and native modules alike); the shell checks every cross-app call against the grants | | **Network** | only the manifest's declared hosts; wider information only through granted tools: the system toolbox (`research`, `crawl`) within the app's declared scope, or octos's own search tools where declared and granted | host network policy on every fetch; the toolbox checks every call against the scope | | **Files** | only the app's folder | app jail and the kernel's per-app workspace | | **Memory** | only `app//…`; promotion by rule or approval | kernel memory namespaces | diff --git a/docs/adr/0004-native-apps-hosting-and-peers.md b/docs/adr/0004-native-apps-hosting-and-peers.md index 0af01898..bb461d8a 100644 --- a/docs/adr/0004-native-apps-hosting-and-peers.md +++ b/docs/adr/0004-native-apps-hosting-and-peers.md @@ -1,7 +1,7 @@ # ADR 0004: Native apps, app agents and cross-app work: one manifest, hosting per target, an agent for every app, approvals by the person - **Date:** 2026-09-28 -- **Status:** Accepted (2026-09-28); amended 2026-09-29 (section 6: `host.ask` dropped for octos's `ask_user_question`; sections 10 and 12: `terminal.run` only where the Terminal runs as a process; factual corrections from the gap review, marked where made; sections 4 and 8: interactive app cards; section 6: one shared conversation per app agent, then the same day two parallel lanes that share history; follow-ups from the 2026-09-29 code review; section 4: which apps have an agent, peers prepared at consent, the shell's "Ask " panel); **Implemented** (2026-09-29): every plan step and every gap from the 2026-09-28 review is on `main`, and the full CI (Desktop, Phone, Apps and services, ROM) passed on `74e1d713`. The follow-ups below remain open work, not part of this decision's implementation. +- **Status:** Accepted (2026-09-28); amended 2026-09-29 (section 6: `host.ask` dropped for octos's `ask_user_question`; sections 10 and 12: `terminal.run` only where the Terminal runs as a process; factual corrections from the gap review, marked where made; sections 4 and 8: interactive app cards; section 6: one shared conversation per app agent, then the same day two parallel lanes that share history; follow-ups from the 2026-09-29 code review; section 4: which apps have an agent, peers prepared at consent, the shell's "Ask <app>" panel); **Implemented** (2026-09-29): every plan step and every gap from the 2026-09-28 review is on `main`, and the full CI (Desktop, Phone, Apps and services, ROM) passed on `74e1d713`. The follow-ups below remain open work, not part of this decision's implementation. *(2026-10-04: three plan items are not on `main`: release packages ship no process app's `bin` (step 2), so a packaged desktop runs the Terminal in-process and read-only; the Windows AppContainer sandbox (step 8, [#137](https://github.com/OctoSense-org/OctoSense/issues/137)); Rinx's data under `apps/rinx/` (step 10, hagency-org/Rinx#37).)* - **Scope:** How OctoSense declares, links, hosts, isolates and trusts its bundled native Rust apps on each target; where every app keeps its data and what its agent may read; how every bundled app, native or script, owns an octos app agent; how the system agent and app agents work across apps; and how the person approves what they do, live or in advance. - **Relates to:** [ADR 0001](0001-one-octosense-repository.md); [ADR 0002](0002-event-driven-app-agents.md) (app agents; amended by this ADR); [Home ADR 0002](home/0002-agentic-app-security-model.md) and [Home ADR 0004](home/0004-system-apps-are-contained-script-apps.md) (amended); Rinx [ADR 0007](https://github.com/hagency-org/Rinx/blob/main/docs/adr/0007-host-owned-octos-app-peers.md); octos UPCR-2026-034 (host-owned app peers, merged), UPCR-2026-035 ([octos#2567](https://github.com/octos-org/octos/pull/2567), host tools per app peer, merged; OctoSense pins its follow-ups, now octos 056173e8 (4a3ec9f9 plus the security fixes #2653, #2655 and #2656, and #2675, hydrate rows that name their tool calls) with octos#2621, #2626, #2636, #2644, #2652, #2654, #2657, #2658 and the UPCR additions #2647 (`read_parent`), #2649 (`peer/purge`) and #2648 (`session/tool_list/set`)) and UPCR-2026-036 (`serve --host-managed`, [octos#2591](https://github.com/octos-org/octos/pull/2591), merged); [octos#2601](https://github.com/octos-org/octos/pull/2601) (tool origin, per-connection ownership); [ADR 0003](0003-shared-octos-client-access.md) (Talk to Octos, OctoSense [#98](https://github.com/OctoSense-org/OctoSense/pull/98): external clients never reach app agents). @@ -11,7 +11,7 @@ | | Native app | Script system app | | --- | --- | --- | -| Today | App Hub (the store and the Card runner), Rinx, Terminal; opt-in Sheets, Reference, AppCard; the native News, Maps and Photos crates once kept "for comparison" were deleted by [#113](https://github.com/OctoSense-org/OctoSense/pull/113) (the script apps are the only versions) | News, Maps, Photos, Camera, Mail, AI providers | +| Today | App Hub (the store and the Card runner), Rinx, Terminal; opt-in Sheets, Reference, AppCard; the native News, Maps and Photos crates once kept "for comparison" were deleted by [#113](https://github.com/OctoSense-org/OctoSense/pull/113) (the script apps are the only versions). *(2026-10-04: also Calculator, Clock, Notes, Reminders and Weather in both shells ([#309](https://github.com/OctoSense-org/OctoSense/pull/309), [#329](https://github.com/OctoSense-org/OctoSense/pull/329)), and Task (Task Manager), a process-only desktop app.)* | News, Maps, Photos, Camera, Mail, AI providers | | Source | a crate pinned in the root `Cargo.toml` | `apps//bundle/`, packed from the same commit (`desktop/system-apps.json`) | | Code | Rust: full process rights, `unsafe`, files, PTYs, threads | OctoScript in App Hub's Card runner; reaches the shell only through `host.request` for families its manifest was granted | | Runs | in the shell process, one splash isolate per instance (`crates/shell/src/module_host.rs`); the isolate separates the script heap only | in the shell process, a nested isolate per instance with `mod.res` and `mod.run` stripped | @@ -34,7 +34,7 @@ | Linux, Vulkan build | DMA_BUF / OPAQUE_FD, zero copy | `MAKEPAD=vulkan` and a Wayland session (Vulkan windowing panics on X11) | | Linux, OpenGL build | CPU readback of every frame | works; costly | -`host::processes_available()` is false for native mobile targets and wasm. Mobile GL contexts are per process. Process apps are built from a checkout (`cargo build --release`, since 2026-09-30; `cargo run` before), then their binary is started; without a checkout the sibling binary is; release packaging does not ship them yet ([#94](https://github.com/OctoSense-org/OctoSense/pull/94) is open). +`host::processes_available()` is false for native mobile targets and wasm. Mobile GL contexts are per process. Process apps are built from a checkout (`cargo build --release`, since 2026-09-30; `cargo run` before), then their binary is started; without a checkout the sibling binary is; release packaging does not ship them yet ([#94](https://github.com/OctoSense-org/OctoSense/pull/94) is open). *(2026-10-04: #94 was closed unmerged on 2026-10-01; release packages ship only `octosense` and the kernel.)* ### Two AI models @@ -110,7 +110,9 @@ Script system apps stay in `desktop/system-apps.json` and `phone/system-apps.jso - **App Hub stays in-process** because it hosts the Card runner that every script app runs in. - **Rinx stays in-process** until the peer link (section 5) and the process sandboxes (section 3) exist; it then moves by a reviewed change to its `hosting` in `native-apps.json`. -A process app that dies takes only itself down: its tile shows it closed with a restart; the shell and other apps keep running. Desktop release packages ship each process app's `bin`. +*(2026-10-04: since [#309](https://github.com/OctoSense-org/OctoSense/pull/309) a native app may have no module (`"module": null`): it runs only as its own desktop process, with hosting `none` where there are no processes. Task (Task Manager) is the first: no shell links it, and the desktop catalog starts it as a process.)* + +A process app that dies takes only itself down: its tile shows it closed with a restart; the shell and other apps keep running. Desktop release packages ship each process app's `bin`. *(2026-10-04: not yet; see the Status.)* **How a process app is built and started** (from the 2026-09-30 code review; `crates/shell/src/clients.rs`, `launch_plan`). In a checkout, the shell first builds the app from the OctoSense workspace, held to its `Cargo.lock`: `cargo build --release --locked --manifest-path /Cargo.toml -p octosense-process-apps -p --bin `, with the `crate` and `bin` of the app's `native-apps.json` entry, so it builds with the versions the shell pins. `crates/process-apps` is a workspace member generated from `native-apps.json` that depends on every app with a `bin`, with the features its binary needs (`bin_features`: Sheets' `standalone`); cargo takes no features for a crate outside the workspace, and without it `-p makepad-sheets` made cargo itself panic. Its own empty library is never built (`--bin`). Cargo runs in the workspace, outside any sandbox, with the shell's allow-listed environment and a `PATH` without entries under the person's home other than the cargo and rustup homes. Then the shell starts the built binary itself under the app's sandbox; cargo never starts the app. The directory the app opens in (the Terminal's inherited working directory) is given to the app alone and never decides which `.cargo/config.toml` cargo reads. The desktop catalog row (`desktop/config/apps.json`) only names and places the app; `tools/native_apps.py --check` fails when a native app's row names another package, binary or source than its entry, or when an app that runs as a process has no row. *(Before 2026-09-30 the launch was `cargo run --release` in the app's working directory, `--locked` only where that checkout had a lock (Makepad's has none), and only the app was sandboxed, as cargo's runner: a `.cargo/config.toml` the Terminal wrote could run a build wrapper unsandboxed at the next launch, confirmed on macOS by a reviewer.)* @@ -122,7 +124,7 @@ In-process modules (all of mobile, and Linux without Vulkan) must meet the bar a - **Native apps are first-party and reviewed only.** A native app is added by a reviewed change to `native-apps.json`. Anything installable from the store is a script app. - **Process apps on the desktop run under an OS sandbox** built from the manifest's `sandbox` and `storage` (files: the jail, its secrets and `external`; network; child processes): macOS sandbox profile, Linux Landlock and seccomp, Windows AppContainer. The terminal's is necessarily broad; most apps' are narrow. -- **Nothing an app can write decides what the shell builds or runs next** (2026-09-30). The build runs outside the sandbox (section 2), so whatever it reads or runs is read-only to every app, after every grant (the Terminal's `home:rw` included): the checkout and its target dir, the cargo and rustup homes (`~/.cargo`, `~/.rustup` by default), every `.cargo/` and `rust-toolchain(.toml)` from the checkout up to the root, and the shell's own directory (`sandbox::Policy::read_only`). The macOS profile applies it; **the Linux Landlock rules do not read `read_only` yet** (`sandbox/linux.rs`, owned by the Linux sandbox work), so on Linux the Terminal's `home:rw` still reaches these paths: open. On macOS the profile also keeps `~/Library/LaunchAgents` unwritable (login items run outside any sandbox), and the generated profiles and per-app logs live in `/sandbox/` and `/logs/clients/` (owner-only, closed to every app), not the shared temp dir. **Consequence for the person: the OctoSense Terminal can no longer write `~/.cargo` or `~/.rustup`, so cargo and rustup updates must run outside it**, in another terminal. Checked live on macOS (2026-09-30, a hidden shell with the Terminal as a sandboxed process): from its shell, writes into `~/.cargo`, `~/.rustup`, the checkout, its `target/` and its `.cargo/`, and `~/Library/LaunchAgents` were refused ("Operation not permitted"), `cargo fetch` of a new crate failed writing `~/.cargo/registry`, and a file in the home was written as before. `cargo install` and `rustup update` write the same folders, so they fail the same way (**unverified**: not run); so does building in the OctoSense checkout. Pointing `CARGO_HOME` elsewhere inside the Terminal should work for other projects (**unverified**). The test `inside_the_terminals_sandbox_the_build_toolchain_and_login_items_are_out_of_reach` proves the refusals on macOS. The keychains are not closed: closing their files broke `git`'s `osxkeychain` credentials in the Terminal (tried 2026-09-30), and the Keychain's own access prompts guard the items. +- **Nothing an app can write decides what the shell builds or runs next** (2026-09-30). The build runs outside the sandbox (section 2), so whatever it reads or runs is read-only to every app, after every grant (the Terminal's `home:rw` included): the checkout and its target dir, the cargo and rustup homes (`~/.cargo`, `~/.rustup` by default), every `.cargo/` and `rust-toolchain(.toml)` from the checkout up to the root, and the shell's own directory (`sandbox::Policy::read_only`). The macOS profile applies it; **the Linux Landlock rules do not read `read_only` yet** (`sandbox/linux.rs`, owned by the Linux sandbox work), so on Linux the Terminal's `home:rw` still reaches these paths: open. *(2026-10-04: closed by [#238](https://github.com/OctoSense-org/OctoSense/pull/238) on 2026-09-30: the Linux Landlock rules grant these paths read and execute only.)* On macOS the profile also keeps `~/Library/LaunchAgents` unwritable (login items run outside any sandbox), and the generated profiles and per-app logs live in `/sandbox/` and `/logs/clients/` (owner-only, closed to every app), not the shared temp dir. **Consequence for the person: the OctoSense Terminal can no longer write `~/.cargo` or `~/.rustup`, so cargo and rustup updates must run outside it**, in another terminal. Checked live on macOS (2026-09-30, a hidden shell with the Terminal as a sandboxed process): from its shell, writes into `~/.cargo`, `~/.rustup`, the checkout, its `target/` and its `.cargo/`, and `~/Library/LaunchAgents` were refused ("Operation not permitted"), `cargo fetch` of a new crate failed writing `~/.cargo/registry`, and a file in the home was written as before. `cargo install` and `rustup update` write the same folders, so they fail the same way (**unverified**: not run); so does building in the OctoSense checkout. Pointing `CARGO_HOME` elsewhere inside the Terminal should work for other projects (**unverified**). The test `inside_the_terminals_sandbox_the_build_toolchain_and_login_items_are_out_of_reach` proves the refusals on macOS. The keychains are not closed: closing their files broke `git`'s `osxkeychain` credentials in the Terminal (tried 2026-09-30), and the Keychain's own access prompts guard the items. - **A process app reaches its agent only through the shell.** It never connects to the kernel (an external client is barred from app agents by UPCR-2026-036) and never sees the host token. - **The shell checks what it relays**: 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 declared schema and size; its own audit log; crash cleanup. - **What no one can check**: what a native app's code does inside its own tool or why it starts a turn. The sandbox and review are the controls. @@ -133,11 +135,11 @@ An app that declares `agent.octos` (native) or `octos.*` capabilities (script) a **Which apps have an agent (amended 2026-09-29).** From the manifests and grants, never from names (`crates/shell/src/agents.rs`, `apps::agent_apps`): a native app whose `native-apps.json` entry grants `agent.octos`; a script app whose manifest declares `octos.*` or an `agent` block, **or whose admitted bundle ships `tools.json`** (App Hub's `AgentBundle::load`). News ships `tools.json` (`news.list`, `news.read`) and declares no `octos.*`: it has an agent. Settings › Assistant › Approvals lists every one of them ("Not asked yet", on, off) before any has asked. An app's own `host.request("octos.*")` stays limited to the services its manifest declares. -**Peers exist once the person allows them.** The first use that asks may be the person opening the app's "Ask " panel, the app's own `octos` call, or the system agent's `agents.ask` (the first-use sheet then shows over the system chat); developer mode asks nobody (section 13), and there is no other bypass. From the moment an agent is allowed (and at startup for the ones allowed before), the shell **prepares its peer**: `peer/prepare`, its tools and `generic_tools` registered (G3), its session open, so the system agent's `peer_list` lists it and `peer_send_input` reaches it without the app ever calling `octos`. A script app's peer is the contained service's `card.`, one per app, shared by the app, its cards, the panel and the system agent. A native app's peer is its instance's and exists while the app is open (a native app with accounts cannot be prepared without its signed-in account). Turning the agent off releases the peer and its contexts at once; the shell refuses the system agent's input to it (the kernel may still list the retired peer). octos lists only prepared peers and offers no hook for an app whose agent is not allowed, so the shell tells the system agent itself: a short note with the system chat's turn whenever the apps' agents changed (each app, and whether it is allowed, not yet allowed or off), and two host tools on the system session, `agents.list` and `agents.ask`, answered by the shell (registered once a host credential exists, that is, once one app's agent has been prepared). +**Peers exist once the person allows them.** The first use that asks may be the person opening the app's "Ask <app>" panel, the app's own `octos` call, or the system agent's `agents.ask` (the first-use sheet then shows over the system chat); developer mode asks nobody (section 13), and there is no other bypass. From the moment an agent is allowed (and at startup for the ones allowed before), the shell **prepares its peer**: `peer/prepare`, its tools and `generic_tools` registered (G3), its session open, so the system agent's `peer_list` lists it and `peer_send_input` reaches it without the app ever calling `octos`. A script app's peer is the contained service's `card.`, one per app, shared by the app, its cards, the panel and the system agent. A native app's peer is its instance's and exists while the app is open (a native app with accounts cannot be prepared without its signed-in account). Turning the agent off releases the peer and its contexts at once; the shell refuses the system agent's input to it (the kernel may still list the retired peer). octos lists only prepared peers and offers no hook for an app whose agent is not allowed, so the shell tells the system agent itself: a short note with the system chat's turn whenever the apps' agents changed (each app, and whether it is allowed, not yet allowed or off), and two host tools on the system session, `agents.list` and `agents.ask`, answered by the shell (registered once a host credential exists, that is, once one app's agent has been prepared). *(2026-10-04: also `agents.provision` and `agents.status` for Mail's incoming-mail agent ([#321](https://github.com/OctoSense-org/OctoSense/pull/321)), and each native app's `agent.system_tools`, shareable read tools lent to the system agent: `calculator.eval`, `clock.now`, `notes.search`, `notes.read`, `reminders.due`, `reminders.list`, `weather.current` ([#309](https://github.com/OctoSense-org/OctoSense/pull/309)); a closed app answers that it is not running.)* -**"Ask " (the person's lane, drawn by the shell).** For every app with an agent the shell draws a panel with the app's conversation (section 6): it opens a sharing context with `open_conversation` (the contained service's for a script app), shows both lanes merged with their speakers ("You", "System agent", "'s agent", "'s agent, to the system agent"), has a composer whose Send stays available whenever the person's own lane is idle (the lanes are independent, so the person can send while the system agent's turn runs) and whose Stop stops only the person's own running turn; the system agent's running turn has its own row with an explicit "Stop the system agent's task" (`broker::interrupt_lane_where`), never in the Send button's place. It shows and answers the app's own questions (G11), and leaves approvals to the shell's sheets. It opens from the bar's "Ask " beside the focused window's title, Shift+F8, or Setup › Assistant › Ask this app's agent; on a desktop it stands beside the system chat, on a phone it is a full-screen sheet. It is the system chat's pane (`crates/shell/src/system_chat/view.rs`) in its app mode (`crates/shell/src/app_chat/`). +**"Ask <app>" (the person's lane, drawn by the shell).** For every app with an agent the shell draws a panel with the app's conversation (section 6): it opens a sharing context with `open_conversation` (the contained service's for a script app), shows both lanes merged with their speakers ("You", "System agent", "<App>'s agent", "<App>'s agent, to the system agent"), has a composer whose Send stays available whenever the person's own lane is idle (the lanes are independent, so the person can send while the system agent's turn runs) and whose Stop stops only the person's own running turn; the system agent's running turn has its own row with an explicit "Stop the system agent's task" (`broker::interrupt_lane_where`), never in the Send button's place. It shows and answers the app's own questions (G11), and leaves approvals to the shell's sheets. It opens from the bar's "Ask <app>" beside the focused window's title, Shift+F8, or Setup › Assistant › Ask this app's agent; on a desktop it stands beside the system chat, on a phone it is a full-screen sheet. It is the system chat's pane (`crates/shell/src/system_chat/view.rs`) in its app mode (`crates/shell/src/app_chat/`). -Three flows: +Four flows: | Flow | Native module | Native app in its own process | Script app | | --- | --- | --- | --- | @@ -146,6 +148,8 @@ Three flows: | 3. The system agent talks to the agent: the system agent's lane, in parallel with flow 1, each lane seeing the other's recent turns | in the kernel; its input reaches the shell as `peer/input` and the shell drives the turn (section 6) | the same, relayed on the peer link | the same | | 4. The person acts in one of the app's cards (glance screen, notification) | as in the app's UI: the card runs under the app's own policy and its calls take the app's path of flows 1 and 2 (the person's lane) | the same | the Card runner's service gate, as in the app's UI (`host.request` under the manifest's grants, then each host service's own checks) | +*(2026-10-04: the native-module column describes Rinx's injected service; every other native agent uses the peer link in process (section 5, [#142](https://github.com/OctoSense-org/OctoSense/issues/142)).)* + **App cards are fully interactive (decided 2026-09-29: "the app card can do everything, for now").** An app's cards on the glance screen and behind notifications run under the publishing app's own policy, the same as the app's UI in the Card runner: typing, buttons, `host.request`, chatting with the app's agent (section 6). For example, the agent drafts an email and publishes it as an editable Mail card; the person edits it and taps Send in the card. A card's calls go through the app's existing path, not the host-tool relay, so the relay's schema checks and budgets (section 3) do not cover them. What the person does in a card is the app's own action (section 8). *(Until 2026-09-29 a glance card was contained: its isolate ran under an empty policy with no host requests, took no input, and the whole tile was one shell tap target that opened the app (`crates/shell/src/glance_card.rs`). Since the branch `feat/interactive-glance-cards`, a tile runs under the publishing app's policy and takes input, an app may publish a `script` card (a Splash program) besides an L0 `source` card, and `glance.publish` with `notify: true` posts a notification that opens the card.)* The shell is always the kernel's host connection: it holds the host token, registers the app's tools, drives the app peer's turns and routes tool calls. @@ -173,12 +177,12 @@ The shell stamps the app's identity from the socket, enforces #2567's host oblig - **A turn the system agent starts on an app peer runs with the app's tools, memory and context: the `peer/input` event** (agreed on #2567). The system agent's `peer_send_input` to a host-owned peer is not run as a kernel continuation; octos delivers it to the host's driving connection as `peer/input {peer, session_id, input_id, turn_id, text}`, and the shell starts the turn on that connection (`turn/start` with the kernel's `turn_id`), so it is host-driven: it has the app's tools and memory, and its approvals are raised in the app's conversation. The system agent follows it on the blackboard. If no host connection holds the peer (never registered, or disconnected), octos runs and queues nothing and tells the system agent the app is not connected; the system agent waits for the app or reports the failure visibly. - **Two parallel lanes that share history, per app agent (decided 2026-09-29).** Each app agent (a host-owned app peer) has two sessions that run **in parallel** and **share history read-only**. *(This supersedes, the same day, the single shared conversation of OctoSense [#166](https://github.com/OctoSense-org/OctoSense/pull/166) and octos [#2626](https://github.com/octos-org/octos/pull/2626): one peer session that the person's turns and the system agent's `peer/input`s both drove, queued one turn at a time. The system agent's long work blocked the person, and the person's chat blocked the system agent.)* - **The system agent's lane** is the peer's own session (`…#peer-`), driven by `peer/input` (the system agent's `peer_send_input`) as before, with its own queue. - - **The person's lane** is a request context (`…#peerctx-.`) that the app opens with `share_history` (octos UPCR-2026-034, "Parallel person context with shared history", [#2636](https://github.com/octos-org/octos/pull/2636), pinned e045c727). A native module opens it with `OctosAppService::open_conversation`, a process app with `octos.session.open` over the peer link, and a script app or card with `host.request("octos.session.open")`. No system agent is involved. The broker opens a new context id for every handle (a closed id is never reopened); the history of a handle still includes the person's lane of the earlier handles of the same account and instance (the shell's "Ask " panel reopened), kept in memory per broker and cleared when the account changes or the contexts are revoked. It starts the person's message there with `origin: {kind: "person", label: }`, or `app` when the app says it started the run itself (G2's `trigger: app | schedule | background | incoming`). G2's trigger stays the authority for approval rules: an unsaid turn is `person` for the label and `Unknown` for rules. Only a shell surface makes a turn the person's for rules (the shell's own composer: the "Ask " panel, the system chat; an in-process module's own UI): an app's `"trigger": "person"` over the wire is only its word (`TurnTrigger::AppSaysPerson`), labelled the person's in the transcript and the app's run for rules, so "when I start it" never answers it (2026-09-30). A person's turn waits only for its own context's previous turn. + - **The person's lane** is a request context (`…#peerctx-.`) that the app opens with `share_history` (octos UPCR-2026-034, "Parallel person context with shared history", [#2636](https://github.com/octos-org/octos/pull/2636), pinned e045c727). A native module opens it with `OctosAppService::open_conversation`, a process app with `octos.session.open` over the peer link, and a script app or card with `host.request("octos.session.open")`. No system agent is involved. The broker opens a new context id for every handle (a closed id is never reopened); the history of a handle still includes the person's lane of the earlier handles of the same account and instance (the shell's "Ask <app>" panel reopened), kept in memory per broker and cleared when the account changes or the contexts are revoked. It starts the person's message there with `origin: {kind: "person", label: }`, or `app` when the app says it started the run itself (G2's `trigger: app | schedule | background | incoming`). G2's trigger stays the authority for approval rules: an unsaid turn is `person` for the label and `Unknown` for rules. Only a shell surface makes a turn the person's for rules (the shell's own composer: the "Ask <app>" panel, the system chat; an in-process module's own UI): an app's `"trigger": "person"` over the wire is only its word (`TurnTrigger::AppSaysPerson`), labelled the person's in the transcript and the app's run for rules, so "when I start it" never answers it (2026-09-30). A person's turn waits only for its own context's previous turn. - **Shared history, read-only.** At the start of each turn in one lane, the kernel shows the model the other lane's last N user and assistant text rows as a labelled, read-only block: "Recent turns in the system agent's conversation with this app" or "Recent turns in the person's conversation with this app". The defaults are N = 20 (at most 50) within 16 KiB. The block is read under the other session's persist lock, drops tool rows, names each row's speaker and is never written into the reader's transcript. A turn still running in the other lane is shown too, after the finished rows (octos [#2644](https://github.com/octos-org/octos/pull/2644)): its request with its speaker, the text it has streamed so far (`[the app agent] (in progress) …`) and a `[turn status]` line (`still running`, `waiting for approval: ` or `waiting for an answer`, never tool arguments). **No shared transcript**: two writers on one transcript are rejected, because they break tool-call pairing and compaction. - **What the system agent sees.** Each of the person's turns also leaves a round on the peer's blackboard (`result-.md` with `origin: person` and `context: `), so `peer_gather` shows it. A person's round alone never fires a fleet synthesis. - **Who speaks, and what the app sees.** #166's speaker labels stay. The kernel's `[from the person: Rinx] …` or `[from the system agent] …` marker leads the user message. The app's follower (`OctosContext::subscribe`, or `conversation` frames on the peer link) gets both lanes' events: its own context's and the system agent's turns on the peer session. Each event carries its `lane` (`person` or `system_agent`), its `speaker`, and a user message's `display_text`; a turn's caller hears only its own turn. `octos.session.history` returns both transcripts merged by `persisted_at`, each row with `lane` and, for a marked user message, `speaker` and `display_text`. Approvals and questions keep going to the shell's router and request model, routed by the turn's origin: the person's and the app's turns to the app's conversation, the system agent's to the system chat. - **Which API opens which.** `open_conversation` opens the person's lane. `open_context(client)` stays a plain request context, with no shared history, no origin and no blackboard round: Rinx's mini apps, and `octos.session.open` with a `client`. - - **Nothing blocks a lane for long, and Stop covers both.** #167's deadlines hold in both lanes. An approval or question nobody answers expires at the prompt deadline (section 8), and a turn still running 30 s later is interrupted (its late calls refused); on the peer session, the next queued input then starts. **Stop from the app stops both lanes**: the person's running turn and the system agent's (the person owns the device, and one Stop is the simplest clear behaviour). This is `octos.turn.interrupt` (`ContextOp::Interrupt`) on a conversation, and the reply lists each stopped turn with its lane. "Stop 's agent" on the shell's app-conversation sheet and question card (`broker::interrupt_where`) does the same. The shell's "Ask " panel is different: its Stop is the person's own lane only, and the system agent's turn is stopped only from its own row (see "Ask " above). + - **Nothing blocks a lane for long, and Stop covers both.** #167's deadlines hold in both lanes. An approval or question nobody answers expires at the prompt deadline (section 8), and a turn still running 30 s later is interrupted (its late calls refused); on the peer session, the next queued input then starts. **Stop from the app stops both lanes**: the person's running turn and the system agent's (the person owns the device, and one Stop is the simplest clear behaviour). This is `octos.turn.interrupt` (`ContextOp::Interrupt`) on a conversation, and the reply lists each stopped turn with its lane. "Stop <app>'s agent" on the shell's app-conversation sheet and question card (`broker::interrupt_where`) does the same. The shell's "Ask <app>" panel is different: its Stop is the person's own lane only, and the system agent's turn is stopped only from its own row (see "Ask <app>" above). - **Accepted hazards.** The two lanes run at the same time on one peer: - File edits are not locked across lanes. Each lane edits its own folder (a context works in `/contexts/`), and a lane that writes the other's folder can race it. - The unknown-outcome marks of host tool calls are shared by both lanes. @@ -207,7 +211,7 @@ Only the person approves. The system agent never does, and cannot be talked into - **The shell is the single path and the authority for every tool call (decided on #110).** Every app tool call, whoever makes it (the owning app's own agent, another app's agent or the system agent), goes kernel `peer/tool/call` → the shell's host connection → the owning app's executor; an app's agent has no direct way to call its own tools. So the shell sees, authorizes (grants, budgets), audits and routes every call. The audit (2026-09-30): the relay records each call when it arrives and when it ends, answered, refused or cancelled (caller, owning app, tool, a SHA-256 digest of the exact arguments, never the arguments, and the outcome), one JSON line each in `logs/tool-calls.jsonl` under the OctoSense home, owner-only; `terminal.run` and `dev.run` are calls like any other. Approvals have their own audit beside it (`logs/approvals-audit.jsonl`). - **Agent tool calls, not the person's own actions (decided 2026-09-29).** The approval router and the shell's sheets govern **agent tool calls** (`peer/tool/call`). **Actions the person takes in an app's own UI, its cards included (section 4), are the app's own actions**: they run under the app's policy and host services' own checks, and the shell draws no extra approval for them. An app can draw something that looks like a shell approval card, but a fake approves nothing: only the shell's host connection answers `approval/respond` (octos approvals are once-only and owned per connection). -- **Hardening deferred (2026-09-29).** OctoSense is at a very early stage; we see how interactive cards work and improve security later. Deferred: a user-gesture check on card actions; letting only the owning app's card act on its tools; a shell-only "Needs you" area; "from " labels on app tiles; a personal anti-phishing mark; filtering mirrored Android notification actions named like approvals; budgets on card-initiated actions. +- **Hardening deferred (2026-09-29).** OctoSense is at a very early stage; we see how interactive cards work and improve security later. Deferred: a user-gesture check on card actions; letting only the owning app's card act on its tools; a shell-only "Needs you" area; "from <app>" labels on app tiles; a personal anti-phishing mark; filtering mirrored Android notification actions named like approvals; budgets on card-initiated actions. - **Who draws the sheet.** For a `confirm: host` tool (for example `mail.send`) the shell draws it, in the app's conversation or batched in the system chat, and a standing rule may answer it. For a `confirm: app` tool the shell hands the confirmation to the owning app, which shows its **own sheet** (for Rinx, its send sheet) for callers of every kind; the call carries who is calling (`caller`, with its context; section 5), so the app's sheet shows it (2026-09-30: as data too, `ConfirmRequest::caller`, so the app can check its own grants; a call cancelled while on the app's sheet is withdrawn from it). When the owning app is not running, or nobody is present, the call waits or is refused visibly, as octos#2567 specifies. **An agent's own text is never an approval surface.** Each line, on either sheet, shows the **owning app, the tool and the exact arguments**, and for a cross-app call the **calling app** (another app's agent, or the system agent). On the shell's sheet that means every argument (2026-09-30): long rows wrap, past twelve rows the argument area scrolls (the wheel, or "More ↓"), control and format characters are shown as their code point (`⟨U+202E⟩`), and Approve once and "Always …" stay disabled until every row has been on screen. A command tool's `command` (`terminal.run`, `dev.run`) is shown one numbered row per line, so a second command after a newline is in plain sight; newlines are allowed, not refused. - **Whose turns the system session relays (2026-10-01).** octos routes a call of the host session's tool set only for a turn the registering connection drove itself (`apply_session_owned_host_tools`, octos ae230ce0): a kernel continuation or wake-up, a Talk to Octos client and another device's connection (ADR 0003) get no host-session tools, so their calls never reach the shell. The system chat therefore relays every call octos sends it for the system session: a turn it started itself is the person's (`trigger: person`), any other turn on its own connection is the system agent's own work (`system_agent`: never "when I start it"), and a call for any other session or an app peer's set is refused at once (`not_this_hosts_session`), not left to time out. An app agent's question routed to the system chat stays listed while it is open; at most 16 open ones are shown, and past that a new one waits, said in the conversation ("N more app questions waiting"), until one settles; none is dropped. - **Live.** Pending approvals surface on a shell-drawn sheet in the app's conversation (on the owning app's own sheet for a `confirm: app` tool), on its card and as a notification; the system agent may batch the `confirm: host` approvals of one request into one shell-drawn sheet in its chat (a plan plus its outward actions, with exact arguments). The shell answers each approval with the person's decision. @@ -231,7 +235,7 @@ One agent for Rinx and one request context per mini app is enough while mini app - Desktop: its own process on macOS and Windows, and on Linux with Vulkan and Wayland (in-process otherwise, section 2). - Its AI may read (`read_screen`, `read_scrollback`) in every hosting, and **type commands only when the Terminal runs as its own sandboxed process**: macOS, Windows, and Linux with Vulkan and Wayland. *(2026-09-30: "sandboxed" is now checked, not assumed: `terminal.run` is registered only after the Terminal's newest launch reported its sandbox applied (`sandbox::launch_sandboxed`), and withdrawn when a launch runs unsandboxed. So on Windows, until its AppContainer exists, there is no `terminal.run`, and none before the Terminal's first launch. Its description no longer says the command runs "unsandboxed": it runs inside the Terminal's own sandbox, which reaches the person's files and the network but not OctoSense's private data or the build toolchain.)* The in-process Terminal (Linux without Vulkan, phones) is read-only: its module offers only the read tools (Makepad `apps/terminal/src/module.rs`, `read_only_manifest`). *(Decided 2026-09-29; this bullet said "type commands, in every hosting".)* Each typed command goes through the shell with a live approval showing the exact command, and is `auto_approvable: false`, so no standing rule approves it (consistent with makepad#41, which already requires confirmation for each run). Outside developer mode, that is: developer mode runs them without a sheet (section 13). -- An agent for the terminal is optional and later. Its output is untrusted text: anything that types into a shell stays behind the person's confirmation and is `auto_approvable: false`. +- An agent for the terminal is optional and later. *(2026-10-04: since [#330](https://github.com/OctoSense-org/OctoSense/pull/330) the Terminal has its own agent, narrowed by `agent.own_tools` to `terminal.read_screen` and `terminal.read_scrollback` over its peer link; `terminal.run` stays the system agent's, behind Command execution.)* Its output is untrusted text: anything that types into a shell stays behind the person's confirmation and is `auto_approvable: false`. - `terminal-ctl`, its control socket for OctoLoop, is off unless the person enables it and is unrelated to the agent. ### 11. App storage: one layout, declared in the manifest, the same for every app @@ -296,6 +300,8 @@ An app agent gets **whatever tools its manifest declares, once the person grants | octos's generic tools (`web_search`, `deep_search`, `deep_research` and the rest) | apps that declare them and are granted them | octos | | **command execution** | apps that declare it and are granted it, and the system agent when the person grants it in Settings (below); each command approved live under section 8 | host tool (for example `terminal.run`, only where a process Terminal exists: section 10) | +*(2026-10-04: what ships is narrower: App Hub admits only `ask_user_question` as a script app's kernel tool (`KERNEL_TOOLS`), and a native agent keeps exactly its entry's `generic_tools`. Only Rinx lists file and memory tools; the Terminal's, Calculator's, Clock's, Notes', Reminders' and Weather's agents use their own tools only.)* + **The system agent's tool set is its grants, enforced by the shell (decided on #110).** Today the system agent's turns get octos's full default tool set, shell included, granted or not. *(Since [#117](https://github.com/OctoSense-org/OctoSense/pull/117), 2026-09-28: the shell writes a `tool_policy` denying `group:runtime` into the `_main` profile, so octos's shell is gone; that policy is a ceiling. Since 2026-10-01 (octos 4a3ec9f9) every kernel start also sets the system session's exact kernel tool list with octos's durable, host-only `session/tool_list/set` (octos#2648); see the follow-ups.)* The system agent gets an explicit list instead: its supervision tools (the peer tools, glance curation, policy and budgets), the toolbox tools it is granted, the cross-app tools granted to it (section 7), and command execution only when the person grants it in Settings, off by default, each command going through the shell with a live approval (section 8). The shell enforces the list (with octos's help where needed), and a test starts a system-agent turn and checks it is offered exactly that list (in particular, no `shell` unless granted). This is a plan step (step 4), and it lands before any cross-app grant reaches the system agent. **Command execution is a grantable capability like any other, and the widest.** A shell escapes every other boundary: the workspace fence (`cat` reads anywhere, including `secrets/`), the network policy (`curl` reaches any host), budgets and audit (one call does anything), and it turns prompt injection into code execution. So the install-time grant says so plainly, and each destructive or outward call goes through section 8. A narrow tool often serves better (for example a sandboxed `toolbox.run_python`: no network, only its folder, time and memory limits). Running real commands on the person's machine is the Terminal app's shareable `terminal.run`: destructive, `auto_approvable: false`, each command approved live with the exact command shown, run in a terminal the person sees. `terminal.run` exists only where the Terminal runs as its own sandboxed process and its launch reported the sandbox applied (macOS and Linux with Vulkan and Wayland today; Windows once its sandbox exists; section 10), so a grant of command execution, the system agent's Settings switch included, takes effect only there; on Linux without Vulkan and on phones it has no target (decided 2026-09-29). Every live approval and `auto_approvable: false` in this section holds outside developer mode, which approves them all (section 13). octos refuses its own `shell` to external clients (#98). For app peers and the system agent, octos's `shell` is kept out by the `_main` profile's `tool_policy`, which denies `group:runtime` (#117), so a granted app reaches command execution through a host tool. *(Corrected 2026-09-29: this sentence said octos refuses `shell` to app peers through #2567's generic-tool allowlist. It does not: with `generic_tools` omitted, as the broker registered every peer until G3, a peer keeps octos's whole roster, `shell` included (octos test `should_offer_exactly_the_registered_tools_and_refuse_an_unlisted_one`); the profile's `tool_policy` is the only barrier (and it failed open until G13: now a policy that cannot be enforced starts no kernel) until each peer's `generic_tools` is set from its grants, plan step 6 (done: each peer is registered with exactly its granted `generic_tools`).)* @@ -322,7 +328,7 @@ How it is turned on and kept from leaking into normal use: ### Mail: "Verify facts" -1. The person taps **Verify facts** on an email. Mail calls `host.request("octos.turn.start", …)` with a structured request ("run the fact-check skill on message "), not free text. +1. The person taps **Verify facts** on an email. Mail calls `host.request("octos.turn.start", …)` with a structured request ("run the fact-check skill on message <id>"), not free text. 2. Mail's agent (instructions in Mail's `AGENT.md`, procedure in a `fact-check` skill) calls `mail.read(id)` (Mail's host service), extracts the **claims**, and searches them with the system toolbox (`toolbox.search`, `toolbox.web_read`; the `research` grant, #108). It searches claims, not the whole email. 3. It builds an L0 card bound to a host-held source (`sys.digest`, #87), renders and checks it, stores it with the email (`mail.save_card(message_id, card)`), and publishes it with `glance.publish`: glance screen and notification. 4. The person acts in the card itself (section 4): asks a follow-up in it, which goes to Mail's agent in the person's lane of its conversation (section 6), which sees the recent turns of the system agent's lane, so it sees the email and the findings; or taps the card's own actions, which are Mail's own actions (section 8). Opening Mail at that email's fact-check thread is still possible, and shows the same conversation, the system agent's exchanges included. *(Until 2026-09-29 tapping the card was the only action: it opened Mail at a thread on a request context per email, whose transcript Mail reloaded with `octos.session.history`, step 3 stored that context's id with the card, and deleting the email closed that context.)* @@ -362,10 +368,10 @@ How it is turned on and kept from leaking into normal use: ### Follow-ups -- [x] **An agent for every app that declares one** (section 4, 2026-09-29): a live run on `625d6cc` found News (tools.json, no `octos.*`) without a peer, an empty `peer_list` for the system agent, Settings listing only Rinx, and no place for the person to talk to an app's agent. Now the shell lists every app with an agent, prepares a consented app's peer, tells the system agent about the ones it cannot list, and draws "Ask ". Still open: a native app's peer exists only while the app is open; the speaker label of a script app's person turns is its app id (the peer's name, kept for resume), not its display name; prepared peers hold a kernel connection, so the kernel's idle stop waits for them. -- [x] **The "Ask " panel's live run, follow-ups** (2026-09-29, [#189](https://github.com/OctoSense-org/OctoSense/pull/189) and the App Hub pin after it): the panel orders both lanes by one clock (each lane's `cursor.seq` is its own; a turn's request line stands where the turn started, handed over by the broker with `turn/started`, since octos writes a turn's user message only when it ends and never for a stopped turn); Stop says "interrupted" once; the system agent is given each peer's slug for `peer_send_input`; and a script app's agent may keep octos's `ask_user_question` (App Hub's one kernel tool for contained apps, `KERNEL_TOOLS`, "Ask you questions" in the store), handed to its peer's `generic_tools` by G3. News's agent keeps it, and its questions of the person's lane are asked in "Ask News". +- [x] **An agent for every app that declares one** (section 4, 2026-09-29): a live run on `625d6cc` found News (tools.json, no `octos.*`) without a peer, an empty `peer_list` for the system agent, Settings listing only Rinx, and no place for the person to talk to an app's agent. Now the shell lists every app with an agent, prepares a consented app's peer, tells the system agent about the ones it cannot list, and draws "Ask <app>". Still open: a native app's peer exists only while the app is open; the speaker label of a script app's person turns is its app id (the peer's name, kept for resume), not its display name; prepared peers hold a kernel connection, so the kernel's idle stop waits for them. +- [x] **The "Ask <app>" panel's live run, follow-ups** (2026-09-29, [#189](https://github.com/OctoSense-org/OctoSense/pull/189) and the App Hub pin after it): the panel orders both lanes by one clock (each lane's `cursor.seq` is its own; a turn's request line stands where the turn started, handed over by the broker with `turn/started`, since octos writes a turn's user message only when it ends and never for a stopped turn); Stop says "interrupted" once; the system agent is given each peer's slug for `peer_send_input`; and a script app's agent may keep octos's `ask_user_question` (App Hub's one kernel tool for contained apps, `KERNEL_TOOLS`, "Ask you questions" in the store), handed to its peer's `generic_tools` by G3. News's agent keeps it, and its questions of the person's lane are asked in "Ask News". -- [x] **Process launch review (2026-09-30)**: done in this change. Process apps are built from the workspace with `--locked` and started directly (no `cargo run` in the app's directory; section 2); the build's paths are read-only in every app's sandbox, macOS profiles and client logs moved into the OctoSense home, login items unwritable (section 3); the desktop catalog rows are checked against `native-apps.json`; a slot whose child was reaped never signals that process group again; the third close within 5 s ends an app that keeps refusing (section 2); `terminal.run` needs a launch that reported its sandbox (sections 10, 12). Still open from it: the Linux Landlock rules do not apply `read_only` yet; AppCard opened with `MAKEPAD_WM_TEST_APP=appcard` in a hidden shell leaves the shell with no window, on `main` too (not caused by this change; not investigated). +- [x] **Process launch review (2026-09-30)**: done in this change. Process apps are built from the workspace with `--locked` and started directly (no `cargo run` in the app's directory; section 2); the build's paths are read-only in every app's sandbox, macOS profiles and client logs moved into the OctoSense home, login items unwritable (section 3); the desktop catalog rows are checked against `native-apps.json`; a slot whose child was reaped never signals that process group again; the third close within 5 s ends an app that keeps refusing (section 2); `terminal.run` needs a launch that reported its sandbox (sections 10, 12). Still open from it: ~~the Linux Landlock rules do not apply `read_only` yet~~ (done, [#238](https://github.com/OctoSense-org/OctoSense/pull/238)); AppCard opened with `MAKEPAD_WM_TEST_APP=appcard` in a hidden shell leaves the shell with no window, on `main` too (not caused by this change; not investigated). *Still open after the implementation (2026-09-29):* the Windows AppContainer sandbox ([#137](https://github.com/OctoSense-org/OctoSense/issues/137)); ~~the Linux sandbox has run only in CI~~ (it had never run: no CI job runs the shell's tests on Linux; run on Linux 7.0 with Landlock ABI 8, with Landlock hidden, and with the Terminal sandboxed next to a real octos kernel on Wayland + Vulkan on 2026-09-30, which found and fixed a program reached through a link that could not start (a `cargo run` launch that sandboxed its build with the checkout, the shell's binary and `~/.cargo` writable was found too, and went with the process-launch review's direct launch), [#138](https://github.com/OctoSense-org/OctoSense/issues/138), [#199](https://github.com/OctoSense-org/OctoSense/pull/199)); on Linux a `network: none` process app still reaches local Unix sockets by path (the session bus among them) and TCP to the hub's port on any address, but no other IP or socket family since [#202](https://github.com/OctoSense-org/OctoSense/pull/202); ~~process apps see the system agent's lane only through history until Makepad's peer client reads `conversation` frames; a second message on one conversation handle is refused rather than queued~~ (done in Makepad's client, [makepad#77](https://github.com/OctoSense-org/makepad/pull/77), in the runtime since the makepad e29a0eaa pin; see the gaps below); Rinx still has to send turn triggers and hand over its send sheet; ~~the OpenHarmony build with octos#2619's BoringSSL (`btls-sys`) is untested~~ (2026-09-30: no OctoSense build links it. octos-cli is taken with `default-features = false` and `api,git,ast`, so octos-research's `impersonate` (wreq, BoringSSL) is not in the graph: `cargo tree -p octosense-home --target aarch64-unknown-linux-ohos -i wreq` finds nothing, and the Android kernel artifact builds with the same features. The OpenHarmony Home HAP built with `rom/scripts/build-home-ohos.py` at the octos pin fe08d8e6, which includes #2619. Installing and running it is **unverified**: no OpenHarmony device was connected. A consequence to know: phones and the in-process kernel have no browser-like client for Google's results page). @@ -386,7 +392,6 @@ Gaps between this decision and `main` found after it was accepted (2026-09-28; s - [x] **The `peer/input` route belongs to whichever connection registered the peer's tools last**: octos `set_host_route` (`crates/octos-cli/src/peers/host_tools.rs`) replaces any earlier route. Untested with more than one instance of an app. Done: one instance drives an app's peer (section 5, "One driver per app peer"). In the shell every broker shares the kernel's one connection, and its router hands a peer session's frames to every consumer that named the session. So before this, two instances of one app (two windows of one module) each started the system agent's `peer/input` and each ran the peer's tool calls, and each drew its approvals. Tested in `crates/app-peers/tests/broker.rs` (`two_instances_of_one_app_drive_the_peer_once_and_hand_over_on_close`, scripted kernel, two connections); a real-kernel run with two instances is **unverified**. Not changed: the relay keeps one executor per app (`set_executor`), so an in-process module with two instances that each set one keeps the last. Only Rinx sets one, and it runs one instance per process. - [x] **Script apps never take a native app's name; App Hub's manifest carries ADR 0004's fields** (2026-09-30, from the ADR 0004 code review; [App-Hub#44](https://github.com/OctoSense-org/OctoSense-App-Hub/pull/44) and the App Hub pin after it). A store app could use a native app's id (`terminal`, `rinx`, `apphub`; App Hub reserved only `os.`), and the shell keys a script app's jail and storage folders, spec, tool declarations, executor and consent by that id, and its tools by the id's last segment: it stood in for the Terminal or Rinx. Now App Hub refuses an id, or an id's last segment, that is a native app's or a name the shell acts under (`RESERVED_NAMES`: `system`, `toolbox`, `workflow`, `dev`, `agents`, `octos`, `shell`, `card`, `os`) at its gate, install and admission (`policy::resolve`, `AgentBundle::load`), and the shell checks again against `native-apps.json` (`apps::check_script_app_id`) before it opens a card, loads or installs a script app's tools, sets its storage spec or deletes its folders on uninstall; a shell test fails if `RESERVED_NAMES` lacks a native app. App Hub's `storage` block takes section 11's fields, and its `tools.json` takes `outward` (waits for the person like a destructive tool) and `auto_approvable` (`false`: no standing rule), which the shell passes on (`outward` to the kernel, `auto_approvable` to the approval router). Still open: a store app may still share a system app's namespace (`dev.example.mail` declares `mail.*`), and the relay resolves a dotted grant by name across every owner (`Catalog::owner_of`); ~~Mail's bundle declares no `storage.accounts` yet, and contained peers still use the account `device`~~ (done: Mail declares `storage.accounts: true`, and each Mail account has its own agent identity: its memory namespace, its folder as workspace, its suspension, its startup-check refusal and its conversation's `read_parent` are all keyed by the account, through one rule in `crates/shell/src/host_tools/mod.rs` (`keeps_accounts`/`account_key`: a native entry's `accounts`, or a script app's manifest `storage.accounts`). **One account is active at a time**: a contained app has one live peer, bound to the account the person signed in to last (Mail's `signed_in` time; an account kept before that field counts as the oldest), and rebound when Mail adds or removes an account; rebinding revokes the previous account's open contexts, and that account's agent resumes, with its own memory, when it is active again. Why not one live agent per account: a script app's octos calls name no account (the Card runner has none), so the shell could not tell which agent a call is for, and every live peer holds a kernel connection. The storage block is read from the manifest before any agent is prepared (`lifecycle::prepare_agent_with`), so an agent prepared before the app was opened binds the right account. Removing a Mail account suspends that account's agent and deletes its folder; `should_suspend_mails_agent_and_delete_its_folder_when_its_account_is_removed` runs it with the shipped manifest.) - [x] **In-process modules use the peer-link client** (section 5, 2026-09-30, [#142](https://github.com/OctoSense-org/OctoSense/issues/142)): the module host picks up Makepad's parked peer links and the shell serves them with the process apps' peer link, so an app that uses `makepad_ai_services::peer::OctosPeer` works the same hosted in-process or as a process. The relay runs an app's tools on its executor when it has one (a module that opens the link only to talk keeps its tools there) and down its link otherwise. A module granted no agent holds no link; its requests are answered `no_agent`. A link opened inside a nested call into another module stays the outer module's. Still open: Rinx uses the injected `OctosAppService` until it moves to the client (a Rinx change); the bridge passes the shell's `conversation` frames to a module as to a process, so a module follows both lanes wherever the pinned Makepad client reads them. -- [x] **A shell-native chat with the system agent**: done. `crates/shell/src/system_chat/` (Setup → Assistant → Assistant chat, F8; full screen on a phone) talks to `_main:api:octosense#system` over the shell's own kernel connection, and its approvals go through the approval router, batched per request. - [x] **A shell-native chat with the system agent**: done. `crates/shell/src/system_chat/` (Setup → Assistant → Assistant chat, F8, the desktop dock's Assistant icon right after Applications, or the phone home's Assistant tile beside the group tiles ([#143](https://github.com/OctoSense-org/OctoSense/issues/143)); full screen on a phone) talks to `_main:api:octosense#system` over the shell's own kernel connection, and its approvals go through the approval router, batched per request. ## Open questions diff --git a/docs/adr/0005-app-contract.md b/docs/adr/0005-app-contract.md index e2fd2dfe..2660eb5b 100644 --- a/docs/adr/0005-app-contract.md +++ b/docs/adr/0005-app-contract.md @@ -62,8 +62,8 @@ Within `1.x`: - `octosense-app-contract` is published to a registry, so consumers write `octosense-app-contract = "1"` and Cargo resolves one `1.x` for the whole build. No lockstep, no duplicate copies, no host-alias tricks. - **Registry: crates.io (decided 2026-09-30).** App Hub's source is already public, and Rinx, OctoSense and third-party apps can depend on it with no credentials or setup. A published version can never be deleted, only yanked, so releases go through review. A private registry was the alternative, rejected because every consumer's CI and machine would need a token and outside app developers could not use it. Plain git dependencies cannot work: Cargo never unifies two git commits. -- OctoSense's root `Cargo.toml` states `octosense-app-contract = "1"`, and its `Cargo.lock` records the exact version it ships with (1.0.0 today); apps state the lowest `1.x` they need. Inside OctoSense, OctoSense's lock is what links. -- Rinx's CI should build against the lowest and the highest `1.x` it claims. *Not yet in place (2026-10-01): with only 1.0.0 published both are the same version; the check joins Rinx's CI (`main.yml`) with the first 1.1.0.* +- OctoSense's root `Cargo.toml` states `octosense-app-contract = "1"`, and its `Cargo.lock` records the exact version it ships with (1.0.0 today; *2026-10-04: 1.1.0 since [#312](https://github.com/OctoSense-org/OctoSense/pull/312)*); apps state the lowest `1.x` they need. Inside OctoSense, OctoSense's lock is what links. +- Rinx's CI should build against the lowest and the highest `1.x` it claims. *Not yet in place (2026-10-01): with only 1.0.0 published both are the same version; the check joins Rinx's CI (`main.yml`) with the first 1.1.0.* *(2026-10-04: 1.1.0 is published; the check is still not in Rinx's CI.)* ### 4. Breaking changes @@ -71,7 +71,7 @@ A change that cannot be additive becomes `octosense-app-contract 2.0`. Hosts sup ### 5. Who changes the contract -The contract crate lives in the App Hub repository under `crates/app-contract`, with its own changelog. A pull request that touches it needs review from App Hub and from one app owner (Rinx). *Not yet enforced (2026-10-01): App Hub has no CODEOWNERS; the rule is followed by hand until a CODEOWNERS entry for `crates/app-contract` is added.* CI runs: the fixture corpus, a public-API diff (`cargo public-api` or `cargo semver-checks`) that fails on any non-additive change within `1.x`, and Rinx's build against the new version. +The contract crate lives in the App Hub repository under `crates/app-contract`, with its own changelog. A pull request that touches it needs review from App Hub and from one app owner (Rinx). *Not yet enforced (2026-10-01): App Hub has no CODEOWNERS; the rule is followed by hand until a CODEOWNERS entry for `crates/app-contract` is added.* CI runs: the fixture corpus, a public-API diff (`cargo public-api` or `cargo semver-checks`) that fails on any non-additive change within `1.x`, and Rinx's build against the new version. *(2026-10-04: not in place for Rinx: App Hub's CI runs the fixtures and `cargo semver-checks`, but builds no Rinx.)* ## Consequences diff --git a/docs/adr/README.md b/docs/adr/README.md index 42a5d345..a71b1c87 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -9,16 +9,16 @@ How these decisions fit together in the code on `main`, and which parts are stil | ADR | Title | Status | | --- | --- | --- | | [0001](0001-one-octosense-repository.md) | One OctoSense repository for the shell, its services, the system apps and both packagings | Accepted | -| [0002](0002-event-driven-app-agents.md) | Event-driven app agents: apps think on their own triggers and publish cards to the glance screen | Proposed | +| [0002](0002-event-driven-app-agents.md) | Event-driven app agents: apps think on their own triggers and publish cards to the glance screen | Proposed; partly implemented | | [0003](0003-shared-octos-client-access.md) | Talk to Octos: one kernel for native and external clients (opt-in) | Implemented; Android unverified | -| [0004](0004-native-apps-hosting-and-peers.md) | Native apps, app agents and cross-app work: one manifest, hosting per target, an agent for every app, approvals by the person | Implemented | +| [0004](0004-native-apps-hosting-and-peers.md) | Native apps, app agents and cross-app work: one manifest, hosting per target, an agent for every app, approvals by the person | Implemented; three plan items open | | [0005](0005-app-contract.md) | The app contract: one small, versioned interface between App Hub and every app | Implemented | | [0006](0006-app-studio-on-the-phone.md) | App Studio on the phone | Accepted | | [0007](0007-composable-mail-action-cards.md) | Composable Mail cards with editing, chat and approved actions | Implementation in progress; phone acceptance pending | ## Home (phone shell) decisions, 2026-09-16 to 2026-09-25 -Written in OctoSense-ROM (retired; merged into this repository) `home/docs/adr/` before the repositories merged, and kept here unchanged as history under [`home/`](home/). They keep their own numbers; cite them as "Home ADR 0004". Where one names a path such as `home/src/` or `home/apps/`, read `crates/shell/src/` (the shell; Settings is in `phone/src/`) and `apps/` (see ADR 0001). Their status is as they recorded it. +Written in OctoSense-ROM (retired; merged into this repository) `home/docs/adr/` and kept here as history under [`home/`](home/); on 2026-09-28 Home 0002 and 0004 gained dated amendments and Home 0004's implementation-status line and last Consequences bullet were updated; on 2026-10-04 Home 0001 and 0002 gained dated notes. Cite them as "Home ADR 0004"; inside them "ADR 000N" means a Home ADR, except in those 2026-09-28 additions (this repository's ADR 0004). Paths are relative to the old `home/` or belong to other repositories: `src/` → `crates/shell/src/` (Settings: `phone/src/`), `resources/` and `android/` → `phone/resources/` and `phone/android/`, `octosense-rom/` → `rom/`, OctoSense-System-Apps `apps/` → `apps/` (ADR 0001); `crates/app-policy` and `crates/app-hub-app` are App Hub's; `apps/calendar/cards/` was Octoscript-AppCard's. Their status is as they recorded it. | Home ADR | Title | Date | Status | | --- | --- | --- | --- | diff --git a/docs/adr/README.zh-CN.md b/docs/adr/README.zh-CN.md index 1757d921..4bd8c8d4 100644 --- a/docs/adr/README.zh-CN.md +++ b/docs/adr/README.zh-CN.md @@ -9,16 +9,16 @@ OctoSense 仓库的决策:Shell、Shell 服务、系统应用,以及桌面 | ADR | 标题 | 状态 | | --- | --- | --- | | [0001](0001-one-octosense-repository.md)(英文) | 用一个 OctoSense 仓库承载 Shell、Shell 服务、系统应用和两种打包形态 | 已接受 | -| [0002](0002-event-driven-app-agents.md)(英文) | 事件驱动的应用智能体:应用按自己的触发条件思考,并把卡片发布到一览屏 | 提议中 | -| [0003](0003-shared-octos-client-access.md)(英文) | Talk to Octos:原生与外部客户端共用一个内核(需手动开启) | 已实现;Android 未验证 | -| [0004](0004-native-apps-hosting-and-peers.md)(英文) | 原生应用、应用智能体与跨应用协作:一份清单、按目标平台托管、每个应用都有智能体、由本人批准 | 已实施 | +| [0002](0002-event-driven-app-agents.md)(英文) | 事件驱动的应用智能体:应用按自己的触发条件思考,并把卡片发布到一览屏 | 提议中;部分已实施 | +| [0003](0003-shared-octos-client-access.md)(英文) | Talk to Octos:原生与外部客户端共用一个内核(需手动开启) | 已实施;Android 未验证 | +| [0004](0004-native-apps-hosting-and-peers.md)(英文) | 原生应用、应用智能体与跨应用协作:一份清单、按目标平台托管、每个应用都有智能体、由本人批准 | 已实施;三项计划未完成 | | [0005](0005-app-contract.md)(英文) | 应用契约:App Hub 与每个应用之间一个小而带版本的接口 | 已实施 | | [0006](0006-app-studio-on-the-phone.md)(英文) | 手机上的 App Studio | 已接受 | | [0007](0007-composable-mail-action-cards.zh-CN.md) | 可组合的 Mail 卡片:编辑、聊天与经批准的操作 | 实现中;手机验收待完成 | ## Home(手机 Shell)的决策,2026-09-16 至 2026-09-25 -这些记录在仓库合并前写于 OctoSense-ROM(已停用,并入本仓库)的 `home/docs/adr/`,现原样作为历史保存在 [`home/`](home/) 下。它们保留原编号,引用时写作“Home ADR 0004”。文中出现 `home/src/`、`home/apps/` 等路径时,对应现在的 `crates/shell/src/`(Shell;设置应用在 `phone/src/`)和 `apps/`(见 ADR 0001)。状态为当时记录的状态。 +这些记录写于 OctoSense-ROM(已停用,并入本仓库)的 `home/docs/adr/`,现作为历史保存在 [`home/`](home/) 下;2026-09-28 Home 0002 和 0004 增加了注明日期的修订,Home 0004 的实施状态行和最后一条 Consequences 也已更新;2026-10-04 Home 0001 和 0002 增加了注明日期的说明。引用时写作“Home ADR 0004”;文中的“ADR 000N”指 Home ADR,2026-09-28 增补内容中的除外(指本仓库 ADR 0004)。路径相对于原 `home/` 目录或属于其他仓库:`src/` → `crates/shell/src/`(设置应用在 `phone/src/`),`resources/`、`android/` → `phone/resources/`、`phone/android/`,`octosense-rom/` → `rom/`,OctoSense-System-Apps 的 `apps/` → `apps/`(见 ADR 0001);`crates/app-policy`、`crates/app-hub-app` 属于 App Hub;`apps/calendar/cards/` 曾属于 Octoscript-AppCard。状态为当时记录的状态。 | Home ADR | 标题 | 日期 | 状态 | | --- | --- | --- | --- | diff --git a/docs/adr/home/0001-hybrid-android-launcher-and-system-bridge.md b/docs/adr/home/0001-hybrid-android-launcher-and-system-bridge.md index 7fbcea18..f7a8ffca 100644 --- a/docs/adr/home/0001-hybrid-android-launcher-and-system-bridge.md +++ b/docs/adr/home/0001-hybrid-android-launcher-and-system-bridge.md @@ -2,6 +2,7 @@ - **Date:** 2026-09-16 - **Status:** Accepted +- **Note (2026-10-04):** Since this record, the OctoSense ROM ships Home, the System Bridge and the native Quickstep fork as platform-signed privileged apps without Trebuchet (`rom/vendor/octosense/octosense.mk`); see the [implementation record](../../../phone/docs/android/adr-0001-implementation-record.md). The implementation status below and the platform-key assumption are history. - **Implementation status:** In progress; Home/bridge prototypes and the separate Quickstep layout-service prototype are installed on the approved OnePlus 6. Native API/Binder checks, Android/hosted icon placement storage/UI, public launch with the bridge service unavailable, activity/process recreation, package replacement/reconnection, widget pages, native Done input, current-user shortcut pin/icon/launch, synthetic notification label/icon rendering and cleanup have phone evidence. Home's visible-icon geometry is acknowledged across the authenticated service, with revision/invalidation and same-signer foreign-package rejection checks passing. The ROM-matched native Quickstep/Recents APK now compiles and passes packaging inspection; a deployment module is prepared. Native installation, app-to-Home transitions, broader lifecycle validation and performance parity remain pending; no milestone exit gate is complete. See the [implementation record](../../../phone/docs/android/adr-0001-implementation-record.md). - **Scope:** OctoSense-mobile Android architecture; native-launcher parity and optional system controls - **Initial target:** OnePlus 6, Android 15 / API 35, LineageOS 22.2 diff --git a/docs/adr/home/0002-agentic-app-security-model.md b/docs/adr/home/0002-agentic-app-security-model.md index e699b9d9..2f156846 100644 --- a/docs/adr/home/0002-agentic-app-security-model.md +++ b/docs/adr/home/0002-agentic-app-security-model.md @@ -2,7 +2,7 @@ - **Date:** 2026-09-19 - **Status:** Proposed -- **Implementation status:** Phases 1 to 4 built on 19–20 Sep 2026; phase 5 partly. **Phase 1**: `octosense-app-policy` (`crates/app-policy` in the OctoSense-App-Hub repository) — the manifest, its admission and its resolution into both containers, 27 tests. **Phases 2–4, in the runtime** (Makepad fork, uncommitted): `widgets/src/splash_policy.rs` holds a per-heap policy the host sets once; the `host` bridge now REFUSES a `host.request` outside the granted capabilities before it is queued (§2); a URL gate in `makepad-script-std` is consulted by all three ways out of an isolate — `net.http_request`, `res.http_resource` (artwork) and the `sys.*` data fetches — against the app's exact host list, with `host:port` entries so a host can admit its own asset origin and no other loopback service (§3); a cumulative instruction budget is charged after every evaluation, callback and hook, and an exhausted isolate runs nothing further; the heap ceiling is applied to the isolate's script heap (§4). An isolate the host never put under a policy behaves exactly as before. Verified on a real card: a bundle whose artwork pointed at an outside server drew without its icons and logged nine refusals with zero outbound requests; the same card with bundled artwork drew fully. **Phase 5**: every agent session profile carries the app id and version as provenance; ledger-side tagging and cross-app reads as a granted capability are not built, because no shared ledger exists in these trees yet. Still open: per-app widget vocabulary (registration is process-wide), and a real signature verifier lives in the hub crate rather than here. +- **Implementation status:** Phases 1 to 4 built on 19–20 Sep 2026; phase 5 partly. **Phase 1**: `octosense-app-policy` (`crates/app-policy` in the OctoSense-App-Hub repository) — the manifest, its admission and its resolution into both containers, 27 tests. **Phases 2–4, in the runtime** (Makepad fork, uncommitted) *(2026-10-04: committed in the fork as 03a0362fe on 2026-09-20.)*: `widgets/src/splash_policy.rs` holds a per-heap policy the host sets once; the `host` bridge now REFUSES a `host.request` outside the granted capabilities before it is queued (§2); a URL gate in `makepad-script-std` is consulted by all three ways out of an isolate — `net.http_request`, `res.http_resource` (artwork) and the `sys.*` data fetches — against the app's exact host list, with `host:port` entries so a host can admit its own asset origin and no other loopback service (§3); a cumulative instruction budget is charged after every evaluation, callback and hook, and an exhausted isolate runs nothing further; the heap ceiling is applied to the isolate's script heap (§4). An isolate the host never put under a policy behaves exactly as before. Verified on a real card: a bundle whose artwork pointed at an outside server drew without its icons and logged nine refusals with zero outbound requests; the same card with bundled artwork drew fully. **Phase 5**: every agent session profile carries the app id and version as provenance; ledger-side tagging and cross-app reads as a granted capability are not built, because no shared ledger exists in these trees yet. Still open: per-app widget vocabulary (registration is process-wide), and a real signature verifier lives in the hub crate rather than here. - **Scope:** What may run inside the OctoSense shell, what contains it, and what contains the agent that acts on its behalf. Covers the phone (one shell process, modules linked in) and the desktop (same shell, process hosting available). Does not cover the ROM's own privilege model, which is [ADR 0001](0001-hybrid-android-launcher-and-system-bridge.md). - **Initial target:** OnePlus 6, Android 15 / API 35, LineageOS 22.2; macOS and Linux desktop. diff --git a/docs/ai-services.md b/docs/ai-services.md index df066a4f..9e248bc6 100644 --- a/docs/ai-services.md +++ b/docs/ai-services.md @@ -1,261 +1,198 @@ -# AI services in OctoSense (octos) +# AI services in OctoSense English | [简体中文](ai-services.zh-CN.md) -How the assistant is wired into OctoSense: the octos kernel, provider configuration, app peers and tools. Use the [Cargo.toml](../Cargo.toml) dependency pins when following external code. For calls and the execution model, read the [architecture walkthrough](architecture-walkthrough.md). Status descriptions refer to this checkout. Dated runs below are historical validation records. +This guide covers the assistant's moving parts in the shell: the octos kernel service, AI providers and where keys live, the services apps call (`octos`, `model`, `glance`) and the system toolbox. It sets out the trust model and ends with how to run and test it all locally. It builds on the README's [key concepts](../README.md#key-concepts). [architecture.md](architecture.md) has the internals, and an app developer needs only Design Flow's [AI-SERVICES guide](https://github.com/OctoSense-org/OctoScript-App-Design-Flow/blob/main/docs/AI-SERVICES.md). -Earlier source reviews took place on 2026-09-28 (OctoSense `ad0d738`) and 2026-09-29 (`baa90bd`). Those dates do not date the current status table. - -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). This page does not repeat it: the native app manifest (`native-apps.json`), the system agent's tool set, the approval router, first-use consent and developer mode are described there and only linked from here. - -## Contents - -- [At a glance](#at-a-glance) -- [Architecture](#architecture) -- [The trust model](#the-trust-model) -- [What each kind of app can use today](#what-each-kind-of-app-can-use-today) -- [Planned: event-driven app agents (ADR 0002)](#planned-event-driven-app-agents-adr-0002) -- [Run and test locally](#run-and-test-locally) -- [Source map](#source-map) - -## At a glance - -| Piece | Status | Where | -| --- | --- | --- | -| One octos kernel per shell process, started on first use, restarted after a provider change | Works today (desktop with its packaged `octos-kernel` or `OCTOS_APP_CORE_BIN`, Android, OpenHarmony; none on iOS) | [`crates/kernel`](../crates/kernel/README.md) | -| AI providers: the person's model providers and keys, keys only on host sheets | Works today | [`apps/ai-providers`](../apps/ai-providers/host-service/README.md) | -| The shell's AI entry point (`start`, policy, per-instance offer, QR import) | Works today | [`crates/ai-host`](../crates/ai-host/README.md) | -| Host-owned app peers: one octos peer per granted app/account, owned by the system agent | Implemented for native modules (Rinx) and contained script apps after per-app consent | [`crates/app-peers`](../crates/app-peers/README.md), [Rinx ADR 0007](https://github.com/hagency-org/Rinx/blob/main/docs/adr/0007-host-owned-octos-app-peers.md) | -| AppCard ("Ask anything") on the shell's kernel | Works today, opt-in (`--features app-appcard`), not shipped | [`apps/appcard`](../apps/appcard) | -| A contained script app (system or store) asking the assistant | Implemented through exact declared `octos.*` services, with a kernel and first-use consent by default. Host tools and kernel-tool approvals route through the shell. `OCTOSENSE_CONTAINED_APPS=0` disables access; `1` is a developer consent bypass, not normal setup | [below](#contained-script-apps-system-and-store) | -| One-shot model calls for contained apps (`model`, `model.complete`) | Works today ([#95](https://github.com/OctoSense-org/OctoSense/pull/95)): registered by `crates/ai-host` with the `llm` service, for apps granted `model` ([App-Hub#24](https://github.com/OctoSense-org/OctoSense-App-Hub/pull/24), in the shells' App Hub pin) | [below](#contained-script-apps-system-and-store) | -| News data service (`news`, no model) | Merged ([#69](https://github.com/OctoSense-org/OctoSense/pull/69)); answers `os.*` apps only | [`apps/news/host-service`](../apps/news/host-service/README.md) | -| `glance.publish`: L0/L1 source cards or Splash script cards | Merged ([#72](https://github.com/OctoSense-org/OctoSense/pull/72)); contained apps granted the `glance` capability publish ([#86](https://github.com/OctoSense-org/OctoSense/pull/86)). `sys.digest` integration is a separate runtime path; [#87](https://github.com/OctoSense-org/OctoSense/pull/87) is historical tracking, not a current PR-status claim | [`crates/shell/src/glance.rs`](../crates/shell/src/glance.rs) | -| Approvals, first-use consent, developer mode | Works today ([#120](https://github.com/OctoSense-org/OctoSense/pull/120), [#118](https://github.com/OctoSense-org/OctoSense/pull/118)): the shell's approval router, standing rules and sheets, fed by the AI services bus and by the kernel's `host_tool` approvals through the host-tool relay ([#145](https://github.com/OctoSense-org/OctoSense/pull/145)); consent before an app's agent first runs; developer mode turned on only by the person | [architecture § Approvals](architecture.md#5-approvals) | -| The system agent's tool set | Works today, enforced: octos's shell is never offered (the `_main` profile's `tool_policy`, [#117](https://github.com/OctoSense-org/OctoSense/pull/117)), and every kernel start sets the system session's exact kernel tool list (`session/tool_list/set`, octos#2648). Command execution is the Setup → Assistant → Command execution switch ([#132](https://github.com/OctoSense-org/OctoSense/pull/132)): `terminal.run`, each command approved live, in both module and process hosting where Terminal is available | [architecture § Tools and grants](architecture.md#4-tools-and-grants) | -| Native apps declared once (`native-apps.json`), hosting per target; the Terminal a system app in its own process on the desktop | Works today ([#113](https://github.com/OctoSense-org/OctoSense/pull/113)) | [architecture § Native apps](architecture.md#native-apps-in-process-or-their-own-process) | -| An app's own agent: declarations versus execution | `tools.json`, generic-tool grants, peers and host-service executors are implemented for News/Mail/Calendar/Photos/Maps/Camera/YouTube. `AGENT.md` prompt loading, bundle skills/model selection and automatic triggers remain gaps | [below](#planned-event-driven-app-agents-adr-0002) | -| Native apps in their own process reaching their agent (the peer link, ADR 0004 §5) | Makepad’s client and the shell’s `crates/shell/src/peer_link/` are implemented. No process app currently requests its own agent; Terminal declares tools but does not request a peer. | [architecture § An app and its own agent](architecture.md#an-app-and-its-own-agent) | - -## Architecture +## The moving parts ```mermaid flowchart TB person(["The person"]) - subgraph shell["One OctoSense shell process: the desktop, or Home on a phone"] - providers["AI providers app (os.ai-providers)
contained script app"] - sheet["Host sheets
key, QR, PIN"] - llm["llm host service
writes the provider profile"] - aihost["crates/ai-host
start, policy, offer"] - broker["crates/app-peers broker
one peer per granted app/account"] - runner["Card runner (App Hub)
host services: mail, news, glance, llm, model, octos"] - router["Approval router
crates/shell/src/approvals"] - relay["Shell host-tool relay
declared tools and caller checks"] - bus["AI services bus
crates/shell/src/ai_bus.rs"] - rinx["Rinx (native module)"] - appcard["AppCard (native, opt-in)"] - scripts["Script apps (system and store)"] + provider(["AI provider"]) + subgraph shell["Shell process"] + providers["AI providers app"] + sheets["Host sheets
keys, QR codes, PIN"] + scripts["Script apps
in the Card runner"] + natives["Native modules
Rinx, Notes, Clock, …"] + subgraph aihost["crates/ai-host"] + llm["llm service"] + model["model service"] + octos["octos service"] + broker["app-peers broker
one peer per app and account"] + end + relay["Host-tool relay"] + router["Approval router"] + exec["Tool executors
host services, toolbox, app windows"] + glance["glance service"] end - vault[("Platform secret store
or owner-only profile")] - kernel[("octos kernel, one per shell
profile _main, system agent session")] + term["Terminal
process app in an OS sandbox"] + vault[("Keys")] + kernel[("octos kernel
system agent, app peers")] person --> providers - person --> sheet + person --> sheets providers -- "llm.*" --> llm - sheet -- "llm.sheet.*" --> llm + sheets -- "llm.sheet.*" --> llm llm --> vault - llm -- "restart after a change" --> kernel - aihost --> kernel - aihost --> broker - broker -- "peer/prepare, peer/context/open, turn/start" --> kernel - rinx -- "scoped OctosAppService" --> broker - appcard -- "its own connection" --> kernel - scripts -- "host.request" --> runner - runner -- "octos.* service
broker identity card.<app id>" --> broker - rinx -- "typed tools" --> bus - bus -- "confirm: host calls" --> router - kernel -- "peer/tool/call" --> relay - relay --> runner - kernel -- "approval/requested" --> router - router --> person + llm -- "profile, restart" --> kernel + scripts -- "octos.*" --> octos + scripts -- "model.*" --> model + scripts -- "glance.*" --> glance + octos -- "card.<app id>" --> broker + natives --> broker + term -- "peer link" --> broker + broker -- "OUP" --> kernel + vault -.-> kernel + vault -.-> model + kernel --> provider + model --> provider + kernel -- "peer/tool/call, approvals" --> relay + relay --> router + relay --> exec + exec -- "cards" --> glance + router -- "sheets" --> person ``` -### The octos kernel: one per shell +[`crates/ai-host`](../crates/ai-host/README.md) is the one entry point both shells call. At startup, `start(Host::platform(data_dir))` configures the kernel, installs the host policy, and registers the `llm` and `model` services and, where the shell hosts a kernel, the `octos` service. Around a native module's `create`, `offer` and `finish` hand the new instance the injected assistant service; only Rinx takes it. `shutdown` stops the kernel. -The kernel is a **shell service** ([`crates/kernel`](../crates/kernel/README.md), package `octosense-kernel`), not an app. The shell configures it once at startup; nothing runs until the first consumer calls `octosense_kernel::connect()`, and later consumers share the same process. octos holds a single-writer lock on its data dir, so there is one kernel per core dir. In ordinary mode, when the last connection closes the kernel stops; Talk to Octos keeps the shared kernel alive while enabled; `shutdown()` stops it with the shell (5 s at most). +Outside this picture, the opt-in AppCard prototype opens its own kernel connection, and the desktop's AI pane (Makepad's `aichat`) reaches apps' tools over Makepad's AI services bus. -How it runs, by platform (`crates/kernel/src/launch.rs`, `KernelSource::platform()` in `crates/ai-host`): +### The kernel service -| Platform | Kernel | Core dir (the octos home) | +One octos kernel runs per shell, as a service in [`crates/kernel`](../crates/kernel/README.md). It starts when the first consumer connects, and `launch.rs` decides where it comes from: + +| Platform | The kernel | Its core dir | | --- | --- | --- | -| Desktop (macOS; Windows and Linux untested) | ` serve --stdio --data-dir ` (plus `--config /config.json` if present), where the kernel is an explicit kernel program (`Options::program`), else `$OCTOS_APP_CORE_BIN`, else the packaged `octos-kernel` beside the shell, run only when its receipt names the pinned octos revision and its SHA-256 matches ([desktop/README.md, Build and run](../desktop/README.md#build-and-run)). **With none of these there is no desktop kernel**, and a developer's own `octos serve` is never touched. | `$OCTOS_APP_CORE_DIR`, else `/octos-home/.octos` (`~/.octosense/octos-home/.octos`): OctoSense's own, no longer the person's `~/octos-home/.octos`, whose provider settings it copies once | -| Android (Home) | The APK's `liboctos.so serve --stdio`, built by [`tools/kernel-artifact.py`](../tools/kernel-artifact.py) from the octos revision the root `Cargo.toml` pins | `/octos-home/.octos` | -| OpenHarmony | In process (`octos_cli::embedded::serve_io`), because a HAP may not exec | `/octos-home/.octos` | -| iOS | **None.** Providers are still saved; no app gets an assistant | – | +| Desktop | a child process (`serve --stdio`): the binary `OCTOS_APP_CORE_BIN` names, else the packaged `octos-kernel` whose receipt names the pinned revision; else none | `~/.octosense/octos-home/.octos` | +| Android | a child process: the APK's `liboctos.so` | `/octos-home/.octos` | +| OpenHarmony | inside the shell (`octos_cli::embedded::serve_io`) | `/octos-home/.octos` | +| iOS | none: providers are saved, but no app gets an assistant | – | -With Talk to Octos enabled, the kernel uses host-managed loopback WebSocket transport instead of stdio. Consumers speak the octos UI Protocol (JSON-RPC frames, as `octos serve --stdio` does) over a `Connection`. Each consumer gets the replies to its own requests and the notifications of its own sessions. When the providers change, the kernel restarts and every connection ends with `CloseReason::Restarted`; a consumer reconnects and reopens its sessions (AppCard's transport is the reference). +A desktop copies provider settings once from the person's own `~/octos-home/.octos` and never writes there. Restarts and idle stops are in [architecture.md §1](architecture.md#the-octos-kernel). ### AI providers and the `llm` host service -**AI providers** (`os.ai-providers`, [`apps/ai-providers/bundle`](../apps/ai-providers/bundle)) is where the person chooses the assistant's models: a primary and fallbacks, each from octos's model catalog, with Test connection, and a PIN-protected `OCTOS1E` QR to move them between devices. It opens from **Start → Settings → AI providers** on the desktop and **OctoSense Settings → Accounts → AI providers** on a phone. - -It is a contained script app like any other; the privileged half is the **`llm` host service** ([`apps/ai-providers/host-service`](../apps/ai-providers/host-service/README.md)): - -- It writes the kernel's profile, `/profiles/_main.json` (`config.llm` and the key variables in `config.env_vars`), then restarts the kernel. -- Keys go to the macOS login keychain (service `octos`), on desktop Linux to `/secrets/` (0600), on Android, iOS and HarmonyOS into the app-private profile (0600): octos has no secret store on Android and iOS, and HarmonyOS's embedded kernel would look for one under the shell's own `HOME`. `OCTOSENSE_LLM_VAULT=file` keeps them in the profile for development. -- Keys, PINs and QR codes are typed, drawn and scanned only on the service's **host sheets**. Only a sheet may call `llm.sheet.*`; the app sees masked status (`"set ••••1234"`, `"missing"`). -- It serves **`os.*` apps only** (`"llm is for OctoSense's own apps."`). It manages providers; it has no method that sends a prompt to a model. +**AI providers** (`os.ai-providers`, [`apps/ai-providers/bundle`](../apps/ai-providers/bundle)) is where the person chooses the assistant's models: a primary and fallbacks from octos's model catalog. A PIN-sealed `OCTOS1E` QR code moves them to another device. The app is an ordinary script app with no agent. Its privileged half is the **`llm` host service** ([`apps/ai-providers/host-service`](../apps/ai-providers/host-service/README.md)): -### `crates/ai-host`: the shell's one entry point +- It writes the kernel's profile, `/profiles/_main.json`, then restarts the kernel. +- Keys, PINs and QR codes appear only on its host sheets. Only a sheet may call `llm.sheet.*`; the app sees masked status such as `"set ••••1234"`. +- It serves system apps (`os.*`) only, and no method takes a prompt: `llm.test` sends a fixed one-word ping. -[`crates/ai-host`](../crates/ai-host/README.md) (`octosense-ai-host`) is what both shells call: `start(Host::platform(data_dir))` at startup (configures the kernel, installs the host policy, registers `llm` with the platform's QR import), `handle_event` every event, `offer(module, scope)` / `finish()` around a native module's `create`, and `shutdown()`. +Keys live where the kernel reads them (`vault.rs`): in the login keychain on macOS (service `octos`), in `/secrets/` (mode 0600) on desktop Linux, and in the profile itself on Windows, Android, iOS and OpenHarmony (mode 0600 on the Unix ones). `OCTOSENSE_LLM_VAULT=file` keeps them in the profile everywhere, as the [local run](#run-and-test-locally) does. -The host **policy** grants exact native `octos.*` services from the generated `native-apps.json` declarations (currently Rinx). - -#### Contained-app consent - -Contained apps use first-use consent by default. The environment variable `OCTOSENSE_CONTAINED_APPS` controls this policy: +## The trust model -| Setting | Behavior | +| Rule | How the code holds it | | --- | --- | -| Unset (default) | Ask the person before an app first uses its agent (`ContainedGate::Consent`). | -| `1` | Developer override: skip first-use consent. Declared-service limits and individual tool approval rules still apply. | -| `0` | Turn off the `octos` service for contained apps. | +| **Keys stay with the shell.** | Only the `llm` service writes keys; the kernel and the `model` service read them. No `octos.*` call carries a key, and the app-peers contract has no field for one (`ModelInfo`). | +| **Secrets are typed only on host sheets.** | App Hub accepts a `.sheet.*` call only from a host sheet, never from the app. Password fields are inert in a script app, and the App Hub gate refuses a bundle that declares one. | +| **Apps never speak the kernel protocol.** | Apps reach their agents only through the app-peers broker, which stamps the app's identity on every call. | +| **Least privilege, by exact name.** | An app gets the `octos.*` services it declares, that exist and that the host grants. `octos.` or `octos.admin` grants nothing (`crates/app-peers/src/contract.rs`, `hosted.rs`). | +| **Approvals belong to the person.** | Every approval an app's peer raises goes to the shell's router; the app hears only `approval/handled_by_host`. The system agent cannot approve. Developer mode, which only the person turns on, approves routed calls for the apps it covers ([the order](architecture.md#5-approvals)). | +| **Mail leaves only when the person approves the exact message.** | No agent tool sends mail, and `mail.send` refuses. Only a physical touch on Approve & Send, in the host's review of the exact From, To, subject and body, authorizes sending; developer mode and standing rules cannot. That works on Android touchscreens only for now (`mail_review.rs`, [Composed Mail cards](mail-composable-cards.md)). | +| **A turn carries its origin.** | The shell stamps who started a turn on its tool calls and approvals ([below](#the-calls)). | +| **Memory and files belong to one app and account.** | Each peer has its own memory namespace, `app//acct-`, and at most its own account's folder. | -On first use, the shell shows what the app’s agent may read and use and where the model runs. **Settings → Assistant → Approvals** lists each app’s agent and lets the person turn it off. +How tools are declared, granted and relayed is in [architecture.md §4](architecture.md#4-tools-and-grants). -The policy is selected by `contained_gate_from` in [`crates/ai-host/src/lib.rs`](../crates/ai-host/src/lib.rs). Per-app consent is handled by [`crates/shell/src/approvals/consent.rs`](../crates/shell/src/approvals/consent.rs) ([#120](https://github.com/OctoSense-org/OctoSense/pull/120)); see [architecture § Approvals](architecture.md#5-approvals). +## What each kind of app can use today -### `crates/app-peers`: host-owned app peers +Four kinds of app have agents: Rinx, through the injected service; the other native apps with an agent (Calculator, Clock, Notes, Reminders, Weather and the Terminal), through the peer link ([architecture.md](architecture.md#an-app-and-its-own-agent)); the system script apps, all but AI providers; and store script apps. **Partly** means within the limits named; **–** means it does not apply. -[`crates/app-peers`](../crates/app-peers/README.md) is the broker between an app and the kernel (Rinx [ADR 0007](https://github.com/hagency-org/Rinx/blob/main/docs/adr/0007-host-owned-octos-app-peers.md); kernel side octos UPCR-2026-034): +| | Rinx | Other native apps | System script apps | Store script apps | +| --- | --- | --- | --- | --- | +| An agent, once the person allows it | Works, while Rinx is open and signed in | Works, while the app is open | Works, prepared once allowed and at each startup | Works, if its bundle declares one (`octos.*`, an `agent` block or `tools.json`); prepared once allowed and at each startup | +| Its own UI talks to its agent | Partly: `OctosAppService`, only for its mini apps' private contexts; the person uses the "Ask Rinx" panel | Works: `OctosPeer`, though the shipped apps only serve tools over it | – (none declares `octos.*`) | Works: [the `octos` service](#script-apps-and-the-octos-service) | +| Tools of its own for its agent | Not yet: its tools serve only the AI pane | Works: read tools, run in the open window | Works: run on its host service or the shell's notice service | Not yet: no host service or app executor runs them | +| `AGENT.md` and skills sent with every turn | – | – | Works (only Mail ships them) | Works | +| Events that start its agent | Not yet | Not yet | Partly: Mail's new-mail trigger only | Not yet | +| Cards on the glance screen | Not yet | Not yet | Works, with `glance`; Mail's can carry a reply draft | Works, with `glance` | +| Chat about one of its cards | Not yet | Not yet | Works: Card / Chat (Email / Chat for a Mail reply) | Works: Card / Chat | +| One-shot model calls | – | – | Works, with `model` (Photos) | Works, with `model` | +| The system toolbox | Unused¹ | Unused¹ | Unused¹ | Not yet | +| A model chosen for its agent | Not yet² | Not yet² | Not yet² | Not yet² | -- A native module whose declared `octos.*` services the policy grants gets **one octos peer per account**, created or resumed with `peer/prepare` on the shell's kernel. A module with nothing granted allocates no peer. -- The peer's **owner** is the shell's system agent session, `_main:api:octosense#system`. The kernel mints a host token for the peer; the shell keeps it in `/../app-peers` (0600), outside every app's reach. -- The peer's **memory namespace** is `app//acct-`: per app and per app account (the hash is a non-secret tag of the account id). The shell selects the permitted account workspace; the kernel provisions and verifies it. A kernel without this contract is refused, never replaced by an ordinary session on the profile's memory. -- The app never sees the kernel protocol. It gets a scoped `OctosAppService` for the instance being created, binds its signed-in account, and opens one **request context** per client instance (`peer/context/open`). A request context is a distinct kernel session under the same peer, not another app peer. `open_conversation` creates the human lane with bounded shared history; the peer session handles system-delegated work. A change of account revokes every context of the old account; late events are dropped. -- The operations are `Open`, `History`, `Turn { text }`, `Interrupt` and `Approval { id, approve }`, each gated by its exact service (`octos.session.open`, `octos.session.history`, `octos.turn.start`, `octos.turn.interrupt`; an approval needs `octos.turn.start`). The broker default turn timeout is 180 s. +¹ Only in builds with the `toolbox-peers` feature, and no app declares `research` or `crawl` yet. -### The system agent +² Every agent runs on the providers set in AI providers; the shell does not read a manifest's `model.needs`. -The system agent owns the app peers and uses `peer_send_input` to delegate, then `peer_gather` to obtain results. `agents.list` discovers available app agents; `agents.ask` requests first-use consent/preparation, not the delegated task itself. +Kernel tools are granted, not inherited: Rinx's agent keeps octos's file, memory and web tools, a script app's agent only `ask_user_question` (the one App Hub admits), and the other native apps' agents none. -The person reaches the system chat through F8 or the shell's Assistant entry (`crates/shell/src/system_chat/`), and can talk directly to an app through the desktop “Ask <app>” panel or a card's `sys.chat`. Phone touch navigation has no Ask-app panel entry yet. +No agent acts outside the device on its own. Mail's agent drafts replies (`mail.propose_reply`, `mail.suggest_reply`) and proposes sending them (`mail.propose_send`), but it has no send tool: the person sends from the host's review ([the trust model](#the-trust-model)). -The system agent has a restricted kernel roster (`system_tools.rs`) and selected shell tools such as opt-in `terminal.run`; it does not inherit every app tool and cannot approve for the person. Talk to Octos is a separate client-access option. Automatic trigger orchestration, learning overlays and richer glance ranking remain planned; cards currently sort by priority and recency. +## Script apps and the `octos` service -### Other assistants in the shell +A script app talks to its agent through the four calls of the `octos` host service, which `ContainedOctos` serves ([`crates/ai-host/src/contained.rs`](../crates/ai-host/src/contained.rs)). The shell registers it wherever it hosts a kernel, even with the consent gate turned off, so an app hears why it gets nothing. All of an app's calls go to one peer, `card.`, acting for `device` or, in an app that keeps accounts such as Mail, the signed-in account. The "Ask <app>" panel and the system agent use the same peer. -- **AppCard** ([`apps/appcard`](../apps/appcard)), the "Ask anything" assistant, is a native module that takes its own kernel connection and sessions. It is opt-in (`--features app-appcard`) and not shipped. -- The desktop's **AI pane** seats Makepad's own `aichat` app, which reaches apps' typed tools over the window manager's AI services bus (`crates/shell/src/ai_bus.rs`). Rinx's assistant tools are reached this way today (below). It is separate from the octos app peers. +### The consent gate -## The trust model +`Policy::contained_gate` ([`crates/ai-host/src/lib.rs`](../crates/ai-host/src/lib.rs)) decides whether a call reaches the peer. By default it is `Consent`: the first call from an app the person has not decided on shows the first-use sheet ([`approvals/consent.rs`](../crates/shell/src/approvals/consent.rs)), which says what the agent may read and use and where the model runs. Calls are refused until the person allows the agent. The answer is kept in `approvals/consent.json` in the OctoSense home. `OCTOSENSE_CONTAINED_APPS=1` (`Everyone`) skips the sheet for development, and `0` (`Off`) refuses every call. -| Rule | How it holds today | -| --- | --- | -| **Keys stay with the host.** | Only the `llm` service reads or writes keys; they live in the platform vault or the owner-only profile under the kernel's core dir, never in an app's jail. The app-peers contract carries no credentials (`ModelInfo` has none). No `octos.*` service lets an app choose a provider or submit a key. | -| **Secrets are the host's.** | No app collects a password, PIN, key or one-time code, not even to pass it on. The runtime makes a password field inert in a policed isolate, App Hub's gate refuses a bundle that declares one, and services ask on their own sheets (`.sheet.*` is accepted only from the sheet). | -| **Approvals belong to the person.** | The shell routes both `host_tool` and other kernel-tool approvals through its host-owned router; the app receives `approval/handled_by_host`. `confirm: app` uses an explicitly registered owning-app sheet. A system agent cannot approve for the person. Standing rules and developer mode are user-controlled; tool-specific restrictions still apply, including Terminal's non-auto-approvable command confirmation. See [Approvals](architecture.md#5-approvals). | -| **Delegation and tool execution have separate authority.** | The system agent delegates to app peers; the relay executes declared tools as their owner after grant/schema/policy checks. Cross-app direct tool grants are explicit, not access to all app APIs or databases. The system session separately has reviewed shell tools such as `terminal.run`. | -| **Memory is private per app and account.** | Each peer's memory namespace is `app//acct-`; contexts of one account are never restored under another. Promotion to shared memory is planned (ADR 0002 §9). | -| **Least privilege, by exact name.** | An app gets `declared ∩ supported ∩ host policy` services, compared by exact name: `octos.` or `octos.admin` grants nothing. | +Settings lists every app's agent with an off switch. Turning one off releases its peer at once, and a denied app stays denied until the person turns it on there. Developer mode skips the sheet for the apps it covers. -## What each kind of app can use today +### The calls -### Native modules (Rinx-style) +An app may call only the `octos.*` names its manifest declares: the Card runner's isolate refuses the rest, and the service checks again (`contained::declared`). The calls work on the app's conversation, the person's lane of its peer ([One app agent, two lanes](../README.md#one-app-agent-two-lanes)): -A native module is trusted Rust linked into the shell. **Rinx** ([hagency-org/Rinx](https://github.com/hagency-org/Rinx)) is the reference and, under `Policy::shipped()`, the only one granted the assistant. - -1. The module lists the exact services in `capabilities()`, for example all four of `octosense_app_peers::OCTOS_SERVICES`. -2. The shell calls `ai_host::offer(module, &scope)` before `module.create` and `offer.finish()` after it; the returned `Assistant` lives with the instance and releases it when dropped. -3. In `create`, the module claims its service; `None` means hosted without assistant access, and the module must not fall back to a kernel of its own: - - ```rust - // Option>; None: no assistant for this instance. - let Some(service) = octosense_app_peers::injection::claim("rinx", &handles.scope.to_string()) else { return }; - service.set_account(Some(&user_id)); - let ctx = service.open_context(ContextSpec { account, instance, services })?; - ctx.call(ContextOp::Turn { text }, sink)?; // Data(..)* then Complete(..) - ctx.close(); // instance closed - service.release(); // app closed - ``` - -4. `availability()` reports `Unavailable` (no kernel, not granted, signed out), `Idle`, `Ready` or `Failed`; the app's ordinary UI keeps working in every state. `settings_entry()` is `Host`: the app offers no provider form of its own and points the person to AI providers. +| Call | Arguments | Answer (`r.data`) | +| --- | --- | --- | +| `octos.session.open` | `{}` | `{open: true, model, …}`; `model` names the provider and model, never a key | +| `octos.session.history` | `{}` | the conversation's `messages`, both lanes merged by time, each with its `lane` and speaker | +| `octos.turn.start` | `{text, trigger?, from?}`, `text` up to 32 KiB | `{turn_id, text, speaker, lane}` once the turn ends | +| `octos.turn.interrupt` | `{}` | `{interrupted, turns}`: the running turns of both lanes stop | -**Tools.** Rinx defines its UI assistant tools in `src/assistant/mod.rs` and exposes them through `ServiceExecutor` on the AI services bus. Reads and sends still use Rinx’s own confirmation sheets. +`trigger` says what started the turn: `person` (the app says the person asked), `app`, `schedule` or `background` (its own run), or `incoming` with `from` (content someone else sent). Left out, the turn is `unknown`, the least trusted (`TurnTrigger` in `crates/app-peers/src/contract.rs`). The transcript labels a `person` turn as the person's, but approval rules treat it as the app's own run: only the shell's "Ask <app>" panel vouches for the person. Chat in a card counts as the app's run too, although the shell draws it. Standing rules skip incoming and unknown runs unless a rule opts in. -The kernel supports host-registered tools, and the shell implements their relay. Rinx has not yet declared those peer tools or registered its send sheet with `OctosAppService::set_confirm_sheet`. Tools available on the AI bus are therefore not automatically available to Rinx’s peer. +An app runs one turn at a time, and the broker interrupts a turn after 180 seconds. A script app gets no pushed events, so it reads `octos.session.history`, which includes the system agent's turns. No argument carries an approval decision. Design Flow's guide has [a minimal call](https://github.com/OctoSense-org/OctoScript-App-Design-Flow/blob/main/docs/AI-SERVICES.md#a-minimal-call-and-handling-unavailable). -**Rinx mini apps.** Rinx hosts reviewed OctoScript mini apps and serves them the same four `octos.*` services, each running instance in its own request context of Rinx's peer ([example](https://github.com/hagency-org/Rinx/tree/main/examples/miniapps/matrix-octos-script)). That is Rinx's own mini-app host, for bundles a person imports into Rinx after review; it is not the App Hub install path. +### Errors -### Contained script apps (system and store) +| `r.error` | Cause | +| --- | --- | +| `this app was not granted "octos", which "" needs` | The manifest lacks that name; the isolate answers at once. | +| `no service answers "octos" on this device` | The shell hosts no kernel (iOS). | +| `Waiting for the person to allow this app's agent (OctoSense asks the first time)` | No consent yet, or the person denied it. | +| `The assistant is turned off for apps on this device` | The gate is `Off`. | +| `Unsupported Octos arguments`, `Provide text (at most 32 KiB)` | Arguments outside the table above. | +| `Add an account in the app before using its assistant` | The app keeps accounts and has none yet. | +| `This app already has an assistant turn running` | A second turn while one runs. | +| `no octos kernel: …` | A desktop without a kernel binary ([below](#run-and-test-locally)). | -A script app runs in App Hub's Card runner and reaches the shell only through `host.request(".", args, fn(r){…})`, for a family its manifest was granted. The services a shell registers today (`crates/shell/src/apps.rs`, `crates/ai-host`): +Design Flow's [Errors](https://github.com/OctoSense-org/OctoScript-App-Design-Flow/blob/main/docs/AI-SERVICES.md#errors) table says what an app should show for each. -| Family | Who may call it | What it is | -| --- | --- | --- | -| `mail` | any app granted `mail` | Mail accounts the person signs in to on the host's sheet | -| `calendar` | Calendar's agent as `os.calendar`; no public script capability | Rust event storage and fixed glance-card tools. The script window currently shows instructions; it cannot call this family through a general Calendar capability. | -| `llm` | `os.*` apps only | Managing the assistant's providers (`llm.providers`, `llm.add_provider`, `llm.test`, `llm.import_qr`, …); **no prompt or completion method** | -| `news` | `os.*` apps only (`may_call` in the service) | News data, without a model. The News bundle now declares `news`; its agent exposes `news.list`/`news.read`/`news.notify`. The background fetch timer does not yet trigger an LLM turn. | -| `glance` | apps granted the `glance` capability ([#86](https://github.com/OctoSense-org/OctoSense/pull/86), [App-Hub#22](https://github.com/OctoSense-org/OctoSense-App-Hub/pull/22)) | Publishing L0 cards to the glance screen, under the app's own id (`crates/shell/src/glance.rs`) | -| `model` | apps granted the `model` capability ([App-Hub#24](https://github.com/OctoSense-org/OctoSense-App-Hub/pull/24)) | One-shot `model.complete {task, input, schema, class?, allow_urls?}` and `model.budget` ([#95](https://github.com/OctoSense-org/OctoSense/pull/95), `apps/ai-providers/host-service/src/complete/`, registered in `crates/ai-host/src/lib.rs`): `class` `fast` or `strong`; the host picks the model from the person's providers, validates the reply against the schema, refuses URLs unless asked, keeps a per-app daily budget; no tools, memory or history, and the app never sees a key | -| `octos` | Apps declaring the exact `octos.*` service names, in a shell that hosts a kernel, subject to [contained-app consent](#contained-app-consent) | The app’s assistant, reached through a host-owned peer. The broker identifies the app as `card.`; `peer/prepare` returns its kernel-issued peer slug ([`contained.rs`](../crates/ai-host/src/contained.rs), [#106](https://github.com/OctoSense-org/OctoSense/pull/106)). Call rules are below. | +## One-shot model calls: the `model` service -The four `octos.*` calls have separate argument, origin, approval and reply rules: +Some jobs need one bounded answer rather than an agent; Photos uses one to group photos into memories. An app granted the `model` capability calls `model.complete {task, input, schema, class?, allow_urls?}`, or `model.budget` for its budget ([`complete/`](../apps/ai-providers/host-service/src/complete/mod.rs)): -- **Arguments.** `octos.turn.start` requires non-blank `text` of at most 32 KiB and accepts optional `trigger` (`person`, `app` or `incoming`) and `from`. The other calls accept only `{}`. -- **Origin.** `trigger: "person"` is classified as `AppSaysPerson`. It is an app assertion, not a trusted human gesture, and cannot bypass approval. -- **Approvals.** Both host-tool and other peer approvals route to the shell. Existing grants and approval rules still apply; developer mode answers for the apps it covers. A host without a router declines requests and lists them in `denied_approvals`. See [`host_approvals.rs`](../crates/app-peers/src/host_approvals.rs). -- **Replies.** The host refuses replies larger than 2 MiB. The response table below lists successful calls and common errors. +- It bypasses the kernel: the service reads the same profile and keys and calls the provider itself. The model sees only fixed instructions, the task, the schema and the input. +- `class` is `fast` (the default) or `strong`. The host tries the person's providers in order, that class first. The app learns which class answered, never the provider, model or key. +- The reply must be JSON that validates against `schema`, at most 16 KiB, with no URL unless `allow_urls` is set. A failing reply is retried once, then refused. +- Each app's budget is by default 6 calls a minute, and 100 calls and 100,000 tokens a UTC day, kept in `/.host/model/ledger.json`, outside every app's jail. +- A refusal reads `: `, with `code` one of `capability`, `no_provider`, `rate`, `budget`, `bad_request`, `invalid_output`, `too_large` or `provider`. -A script's own assistant calls use declared `octos.*` services. Separately, the shell can drive an app peer from `agent`/`tools.json` without the script declaring those UI calls. Replies on the script service path: +Design Flow's [One-shot model calls](https://github.com/OctoSense-org/OctoScript-App-Design-Flow/blob/main/docs/AI-SERVICES.md#one-shot-model-calls-model) shows a call. -| Call | Answer | -| --- | --- | -| a family the manifest did not grant | `r.error`: `this app was not granted "", which "" needs` (at once, from the isolate) | -| `octos.turn.start {text}`, granted, kernel and provider configured | `r.data`: `{turn_id, text}`, the reply of the app's peer | -| `octos.*` with arguments beyond the rules | `r.error`: `Unsupported Octos arguments`, or `Provide text (at most 32 KiB)` | -| `octos.*` with the assistant turned off for apps (`OCTOSENSE_CONTAINED_APPS=0`) | `r.error`: `The assistant is turned off for apps on this device` | -| `octos.*` before the person allowed the app's agent | `r.error`: `Waiting for the person to allow this app's agent (OctoSense asks the first time)`, and the shell shows its first-use sheet (read in `contained.rs` and `approvals/mod.rs`; **unverified** in a running shell) | -| `octos.*` on a desktop without a packaged kernel or `OCTOS_APP_CORE_BIN` | `r.error`: `no octos kernel: no packaged octos-kernel beside and no OCTOS_APP_CORE_BIN override; …` (a stale packaged kernel: `no octos kernel: refusing the packaged kernel …`) | -| `octos.*` in a build that links no kernel (iOS) | `r.error`: `no service answers "octos" on this device` (**unverified**) | -| `llm.*` from a store app granted `llm` | `llm is for OctoSense's own apps.` | -| anything in App Hub's `card-host` | `no service answers "" on this device` (`card-host` registers no services) | +## The system toolbox -**Historical validation (not rerun for this revision).** The first row, the `llm` row and the `card-host` row were run in `card-host` (App Hub `362d832`) on 2026-09-27. The reply, the unsupported-arguments answer and the missing-kernel answer were run on macOS on 2026-09-28 in a release desktop with hidden windows, through a system app declaring `octos.session.open` and `octos.turn.start` opened from the launcher; with a kernel built at the then-pinned octos `7bec0918` and the person's provider, the reply came from the peer `card.`, whose memory namespace `app/card./…` appeared in the kernel's data. The text-length and switch-off answers are covered by `cargo test -p octosense-ai-host`, through the same dispatch. OctoScript-App-Design-Flow's [AI-SERVICES](https://github.com/OctoSense-org/OctoScript-App-Design-Flow/blob/main/docs/AI-SERVICES.md) has the example app and the argument and answer shapes of the four `octos.*` calls as Rinx serves them. +The system toolbox ([`crates/toolbox`](../crates/toolbox/README.md)) gives app agents bounded research instead of a browser: fixed OctoScript workflow templates, plus search, page-reading and crawl tools, which the shell offers as host tools owned by `toolbox` ([`toolbox_peers.rs`](../crates/ai-host/src/toolbox_peers.rs)). -The manifest's `agent` and admitted `tools.json` participate in shell agent discovery, tool loading and grants. News/Mail/Calendar/Photos/Maps/Camera/YouTube have working host-service-backed declarations. The `profile`/model requirements, `AGENT.md` prompt content, bundle skills and automatic triggers are not fully consumed; `implemented_by: "app"` has no script executor. Mail currently declares only `mail.notify`, not its UI's read/send methods. The agent workspace does not automatically expose host-service databases. Rinx mini-app admission is a separate contract; do not assume App Hub metadata is supported there. +- **What each capability grants.** `research` grants `workflow.run`, `workflow.fork`, `toolbox.search` and `toolbox.web_read`; `crawl` grants `toolbox.deep_crawl`. +- **When they are offered.** Only in builds with the `toolbox-peers` feature (the phone's default build, not the desktop's), only after consent; among script apps, for now only to system apps. The system agent gets none. +- **Budget and results.** Templates call the model through the `model` service, so they share the app's budget, and write results to `/.host/toolbox/`, where the app cannot forge them. -## Planned: event-driven app agents (ADR 0002) +## Glance cards -[ADR 0002](adr/0002-event-driven-app-agents.md) (status **Proposed**) describes the broader plan for event-driven app agents. App peers, tool execution and human conversations already work. Automatic triggers and other remaining pieces are listed separately below; the ADR’s proposed status does not mean every component is still planned. +An app with the `glance` capability publishes cards to the glance panel (desktop) or glance page (phone) through the `glance` service ([`crates/shell/src/glance.rs`](../crates/shell/src/glance.rs)): `glance.publish`, `glance.withdraw` and `glance.list`. The shell takes the publisher from the caller, never from the arguments, binds the card to the account it was published under, and lets each app publish 6 times a minute and keep 4 cards. System apps' agents publish through their own tools: `.notify` fills a fixed card template with the model's text, and Mail's `mail.publish_card` checks a card the model wrote and, given a `draft_id`, binds it to a host-owned reply draft. -| Piece | ADR | Status | -| --- | --- | --- | -| `tools.json`: names, input/output schemas, risk, confirmation, sharing and implementation | §4, §12 | Loaded by `host_tools/script_apps.rs`, registered on peers and routed to host-service executors. News, Mail, Calendar, Photos, Maps, Camera and YouTube ship declarations; AI providers has no app agent. Arbitrary `implemented_by: "app"` script execution remains unavailable. | -| `AGENT.md`, data-only skills, model requirements, `background`, triggers | §2, §3 | Admitted metadata. Prompt/skill loading, model selection from these requirements and trigger scheduling remain unimplemented in the shell. Tool background eligibility alone does not schedule a turn. | -| Kernel: host-registered tools per peer (`peer/tools/register`, `peer/tool/call`/`result`/`cancel`), tool-list and risk enforcement, approvals only on the host connection, an allowlist of generic tools, `peer/input` | §4, §13 | Kernel side merged: [octos#2567](https://github.com/octos-org/octos/pull/2567) (UPCR-2026-035), in the pin with its follow-ups (octos#2616). The shell side is on main: [#145](https://github.com/OctoSense-org/OctoSense/pull/145) (tool registration, the relay, `peer/input`) and G3 (per-app `generic_tools` from the manifests, `tools.json` loading, grants, executors for script apps' host services, schema and budget checks; `peer/input/reject`, octos#2621). | -| Tool approval policy | §4, §12 | Destructive and outward calls enter the shell’s approval router. It applies standing rules, host or registered app confirmation surfaces, and developer mode. Grants, schemas and caller budgets are checked separately. See [Approvals](architecture.md#5-approvals). | -| News M1: the `news` data service (no model) | First slice | Merged ([#69](https://github.com/OctoSense-org/OctoSense/pull/69)); issue [#60](https://github.com/OctoSense-org/OctoSense/issues/60). | -| News M2: `os.news` gets a peer and tools | First slice | Implemented: manifest `agent`, `news.list`/`news.read`/`news.notify` tools, shell peer preparation and relay. | -| News M3: triggers and an unattended run | First slice | Planned: [#62](https://github.com/OctoSense-org/OctoSense/issues/62). | -| News M4: glance cards (`glance.publish`, the glance page and desktop panel) | §7, §8 | `glance.publish` ([#72](https://github.com/OctoSense-org/OctoSense/pull/72)) and the `glance` capability for contained apps ([#86](https://github.com/OctoSense-org/OctoSense/pull/86), with [App-Hub#22](https://github.com/OctoSense-org/OctoSense-App-Hub/pull/22)) merged; `sys.digest` integration has separate runtime/host requirements; [#87](https://github.com/OctoSense-org/OctoSense/pull/87) is historical tracking; issue [#63](https://github.com/OctoSense-org/OctoSense/issues/63). | -| M5: the system toolbox as granted tools | §6 | Implemented behind `toolbox-peers`: `crates/ai-host/src/toolbox_peers.rs` and the shell toolbox executor. Home enables the feature by default; desktop opts in. Tools still require admission/grants and consent; this is not a general conversation with the system agent. | -| M6: render and critique (`card-studio`) | §7 | App Hub's `card-studio` crate merged ([App-Hub#19](https://github.com/OctoSense-org/OctoSense-App-Hub/pull/19)); the skill is planned ([#65](https://github.com/OctoSense-org/OctoSense/issues/65)). | -| M7: app conversations, questions and memory | §9, §10 | Implemented human/system lanes, Ask-app panel, `sys.chat`, host-routed questions/approvals and scoped peer/context memory. See the [walkthrough](architecture-walkthrough.md). | -| M8: the outer loop (overlays on `AGENT.md`) | §11 | Planned: [#67](https://github.com/OctoSense-org/OctoSense/issues/67). | - -The tracking issue is [#68](https://github.com/OctoSense-org/OctoSense/issues/68). App Hub's [PUBLISHING § The app's agent and tools](https://github.com/OctoSense-org/OctoSense-App-Hub/blob/main/docs/PUBLISHING.md#the-apps-agent-and-tools) has the file formats. +The phone's feed shows only summaries and runs no generated UI. Opening a card shows its workspace, which keeps its state between openings: full screen on the phone, centred on the desktop. It has Card / Chat tabs when the publisher has an agent, even without `sys.chat`, and Email / Chat over one saved draft for a Mail reply. The README's [Cards and questions](../README.md#cards-and-questions) covers the workspace; [Composed Mail cards](mail-composable-cards.md) covers Mail's drafts, review and tests. ## Run and test locally -The build/launch recipes below are **unverified**; read dependency versions from the root Cargo.toml. +Except `--plan`, the commands below are **unverified**. ### Desktop, with a throwaway kernel and profile -1. Build the desktop kernel from the OctoSense repository root. The existing helper reads the octos revision from `Cargo.lock` and uses its own `target/octos-kernel/` directory, leaving other checkouts alone. The build and subsequent launch steps are **unverified**; only the `--plan` output was checked for this revision. +1. Build the desktop kernel from the repository root. The helper reads the octos revision from `Cargo.lock` and builds in its own `target/octos-kernel/`, leaving other checkouts alone; `--plan` prints the steps without running them. ```sh python3 tools/kernel-artifact.py --host --plan @@ -273,44 +210,40 @@ The build/launch recipes below are **unverified**; read dependency versions from cargo run --release -p octosense ``` - The log says `octos: kernel service ready (starts on first use), core dir …`. Without `OCTOS_APP_CORE_BIN` it uses the packaged `octos-kernel` beside the shell if there is one (`python3 tools/kernel-artifact.py --host --stage target/release`), else says there is no kernel, and AI providers still saves providers. + The log says `octos: kernel service ready (starts on first use), core dir …`. Without `OCTOS_APP_CORE_BIN` the desktop uses a packaged `octos-kernel` beside the shell (`python3 tools/kernel-artifact.py --host --stage target/release` stages one); with neither, the log says there is no kernel, and AI providers still saves providers. -3. Open **Start → Settings → AI providers**, add a model (family, model, route, key, **Test connection**, save). The profile is `$T/octos-home/.octos/profiles/_main.json`; with the file vault the key is in it, so delete `$T` afterwards. -4. Use the assistant through a consumer: Rinx (linked by default and in-process; open it from the launcher; sign in to Matrix, allow its agent on the first-use sheet, then use its assistant), or AppCard (`--features app-appcard`). A contained app reaches it through the `octos` host service ([above](#contained-script-apps-system-and-store)); leave `OCTOSENSE_CONTAINED_APPS` unset and allow the app's agent when the shell asks. F8 also opens the system chat; use `agents.ask` for consent/preparation, then `peer_send_input` for delegation. +3. Open AI providers (Start → Settings → AI providers) and add a model with its key. The profile is `$T/octos-home/.octos/profiles/_main.json`; with the file vault the key is in it, so delete `$T` afterwards. +4. Talk to the assistant: F8 opens the system chat, and an app's "Ask <app>" panel reaches that app's agent once you allow it on the first-use sheet. A script app of your own reaches its agent through [the `octos` service](#script-apps-and-the-octos-service), with `OCTOSENSE_CONTAINED_APPS` left unset. AppCard (`--features app-appcard`) is another consumer. -**Hidden windows.** Add `MAKEPAD_HIDE_WINDOWS=1 MAKEPAD_REMOTE=` to drive the shell over the remote bridge without taking the screen ([desktop README § Remote-control bridge](../desktop/README.md#remote-control-bridge)). `desktop/scripts/ai_providers_remote.sh` runs AI providers end to end this way with fake keys and outbound HTTPS denied, and `desktop/scripts/glance_remote.sh` does the same for the glance panel. +**Hidden windows.** Add `MAKEPAD_HIDE_WINDOWS=1 MAKEPAD_REMOTE=` to drive the shell over the remote bridge without taking the screen ([desktop README § Remote-control bridge](../desktop/README.md#remote-control-bridge)). `desktop/scripts/ai_providers_remote.sh` runs AI providers end to end this way, with fake keys and outbound HTTPS denied; `desktop/scripts/glance_remote.sh` drives the glance panel the same hidden way. **Tests** (from the repository root): ```sh cargo test --locked -p octosense-kernel # against a stand-in kernel cargo test --locked -p octosense-app-peers --features octos-core,ws # the broker, scripted kernel -cargo test -p octosense-ai-host --features octos-core,llm +cargo test --locked -p octosense-ai-host --features octos-core,llm cargo test --locked -p octosense-shell --lib approvals # router, rules, sheets, consent, contacts # The real kernel (build it as in step 1): -OCTOS_CORE_TEST_KERNEL=/path/to/octos cargo test -p octosense-kernel --test real_kernel -- --nocapture -OCTOS_APP_PEERS_TEST_KERNEL=/path/to/octos cargo test -p octosense-app-peers --features octos-core --test real_kernel -- --nocapture +OCTOS_CORE_TEST_KERNEL=/path/to/octos cargo test --locked -p octosense-kernel --test real_kernel -- --test-threads=1 --nocapture +OCTOS_APP_PEERS_TEST_KERNEL=/path/to/octos cargo test --locked -p octosense-app-peers --features octos-core --test real_kernel ``` ### Phone -- **Android (Home):** the Home APK bundles the kernel as `liboctos.so`. The configured `rom/scripts/build-home.py` pipeline builds the APK pair (see [phone/README.md](../phone/README.md)), using [`tools/kernel-artifact.py`](../tools/kernel-artifact.py) at the locked octos revision. The kernel starts on first use. -- Configure providers in **OctoSense Settings → Accounts → AI providers**, or move them from a desktop: **Show QR for phone** on the desktop, then import on the phone by camera, image or pasted code, with the PIN. -- **OpenHarmony** runs the kernel in process. **iOS** has no kernel: providers are saved, no app gets an assistant. +- **Android (Home)** bundles the kernel as `liboctos.so`. `rom/scripts/build-home.py` builds the APK pair ([phone/README.md](../phone/README.md)), with [`tools/kernel-artifact.py`](../tools/kernel-artifact.py) at the locked octos revision. The kernel starts on first use. +- Configure providers in OctoSense Settings → Accounts → AI providers, or import a desktop's code (Show QR for phone) by camera, image or pasted text, with its PIN. +- OpenHarmony and iOS: see [the kernel service](#the-kernel-service). ## Source map | What | Where | | --- | --- | -| Kernel service | [`crates/kernel`](../crates/kernel/README.md) (`src/launch.rs`, `src/lib.rs`) | -| Shell entry point, policy, offer | [`crates/ai-host/src/lib.rs`](../crates/ai-host/src/lib.rs) | +| Entry point, host policy, `offer` | [`crates/ai-host/src/lib.rs`](../crates/ai-host/src/lib.rs) | +| The kernel service | [`crates/kernel/src`](../crates/kernel/src) (`launch.rs`, `router.rs`, `system_tools.rs`) | | App peers: contract, broker, shell side | [`crates/app-peers/src`](../crates/app-peers/src) (`contract.rs`, `broker.rs`, `hosted.rs`) | -| `llm` service, vault, sheets | [`apps/ai-providers/host-service/src`](../apps/ai-providers/host-service/src) | -| `octos` service for contained apps | [`crates/ai-host/src/contained.rs`](../crates/ai-host/src/contained.rs) | -| `model` service | [`apps/ai-providers/host-service/src/complete`](../apps/ai-providers/host-service/src/complete) | -| Host services a shell registers | `crates/shell/src/apps.rs` (`register_host_services`), [`crates/shell/src/glance.rs`](../crates/shell/src/glance.rs) | -| Approvals, consent, contacts, Settings page; developer mode | [`crates/shell/src/approvals/`](../crates/shell/src/approvals/mod.rs), [`crates/shell/src/dev_mode.rs`](../crates/shell/src/dev_mode.rs) | -| The system agent's tool set | [`crates/kernel/src/system_tools.rs`](../crates/kernel/src/system_tools.rs) | -| Everything else (native app manifest, hosting, storage, protocols) | [architecture § Source map](architecture.md#source-map) | -| Capabilities, `tools.json`, agent fields | OctoSense-App-Hub [`crates/app-policy/src`](https://github.com/OctoSense-org/OctoSense-App-Hub/tree/main/crates/app-policy/src) (`manifest.rs`, `services.rs`, `agent.rs`) | -| Rinx's assistant and mini-app adapter | hagency-org/Rinx `src/assistant/`, `src/host/octos.rs` | +| The `octos` service | [`crates/ai-host/src/contained.rs`](../crates/ai-host/src/contained.rs) | +| The `llm` and `model` services, the key vault | [`apps/ai-providers/host-service/src`](../apps/ai-providers/host-service/src) (`lib.rs`, `vault.rs`, `complete/`) | +| Which apps have agents, and their preparation | [`crates/shell/src/apps.rs`](../crates/shell/src/apps.rs) (`agent_apps`), [`crates/shell/src/agents.rs`](../crates/shell/src/agents.rs) | +| Mail drafts and the send review | [`apps/mail/host-service/src/drafts.rs`](../apps/mail/host-service/src/drafts.rs), [`crates/shell/src/mail_review.rs`](../crates/shell/src/mail_review.rs) | +| Everything else | [architecture.md § Source map](architecture.md#source-map) | diff --git a/docs/ai-services.zh-CN.md b/docs/ai-services.zh-CN.md index 0d9db4ba..3f20a8a3 100644 --- a/docs/ai-services.zh-CN.md +++ b/docs/ai-services.zh-CN.md @@ -1,259 +1,198 @@ -# OctoSense 中的 AI 服务(octos) +# OctoSense 中的 AI 服务 [English](ai-services.md) | 简体中文 -本文介绍 OctoSense 的 octos 内核、提供方配置、应用 peer 和工具。阅读外部源码时使用 [Cargo.toml](../Cargo.toml) 的依赖锁定版本。调用链与执行模型见[架构导读](architecture-walkthrough.zh-CN.md)。状态说明以当前检出为准。下方带日期的运行信息是历史验证记录。 +本文介绍 Shell 中助手的各个组成部分:octos 内核服务、AI providers 与密钥的存放位置、应用调用的服务(`octos`、`model`、`glance`)以及系统工具箱。文中说明信任模型,最后介绍如何在本地运行和测试这一切。本文以 README 的[关键概念](../README.zh-CN.md#关键概念)为基础。内部实现见 [architecture.zh-CN.md](architecture.zh-CN.md);应用开发者只需读 Design Flow 的 [AI-SERVICES 指南](https://github.com/OctoSense-org/OctoScript-App-Design-Flow/blob/main/docs/AI-SERVICES.zh-CN.md)。 -历史源码评审分别在 2026-09-28(OctoSense `ad0d738`)和 2026-09-29(`baa90bd`)进行;这些日期不代表当前状态表的更新时间。 - -本文讨论的是 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)。本文不重复那些内容:原生应用清单(`native-apps.json`)、系统 Agent 的工具集、审批路由、首次使用同意和开发者模式都在那里描述,这里只给出链接。 - -## 目录 - -- [概览](#概览) -- [架构](#架构) -- [信任模型](#信任模型) -- [各类应用目前能用什么](#各类应用目前能用什么) -- [规划中:事件驱动的应用 Agent(ADR 0002)](#规划中事件驱动的应用-agentadr-0002) -- [本地运行与测试](#本地运行与测试) -- [源码位置](#源码位置) - -## 概览 - -| 组成部分 | 状态 | 位置 | -| --- | --- | --- | -| 每个 Shell 进程一个 octos 内核,首次使用时启动,提供方变更后重启 | 目前可用(有随附 `octos-kernel` 或设置了 `OCTOS_APP_CORE_BIN` 的桌面端、Android、OpenHarmony;iOS 没有) | [`crates/kernel`](../crates/kernel/README.md) | -| AI providers:用户的模型提供方和密钥,密钥只在宿主面板上输入 | 目前可用 | [`apps/ai-providers`](../apps/ai-providers/host-service/README.md) | -| Shell 的 AI 入口(`start`、策略、按实例提供服务、QR 导入) | 目前可用 | [`crates/ai-host`](../crates/ai-host/README.md) | -| 宿主拥有的应用 peer:每个获授权的应用/账号一个 octos peer,归系统 Agent 所有 | 已实现:原生模块(Rinx)及经过按应用同意的隔离脚本应用 | [`crates/app-peers`](../crates/app-peers/README.md)、[Rinx ADR 0007](https://github.com/hagency-org/Rinx/blob/main/docs/adr/0007-host-owned-octos-app-peers.md) | -| 在 Shell 内核上运行的 AppCard("Ask anything") | 目前可用,需主动开启(`--features app-appcard`),不随产品发布 | [`apps/appcard`](../apps/appcard) | -| 隔离脚本应用向助手提问 | 已实现:精确声明的 `octos.*` 服务、内核与默认的首次使用同意;宿主工具和内核工具审批都交给 Shell。`OCTOSENSE_CONTAINED_APPS=0` 禁用;`1` 是跳过同意的开发覆盖,不是正常设置步骤 | [见下文](#隔离运行的脚本应用系统应用和商店应用) | -| 供隔离应用使用的一次性模型调用(`model`,`model.complete`) | 目前可用([#95](https://github.com/OctoSense-org/OctoSense/pull/95)):由 `crates/ai-host` 与 `llm` 服务一同注册,供获得 `model` 权限的应用使用([App-Hub#24](https://github.com/OctoSense-org/OctoSense-App-Hub/pull/24),已在 Shell 锁定的 App Hub 中) | [见下文](#隔离运行的脚本应用系统应用和商店应用) | -| News 数据服务(`news`,不使用模型) | 已合入([#69](https://github.com/OctoSense-org/OctoSense/pull/69));只响应 `os.*` 应用 | [`apps/news/host-service`](../apps/news/host-service/README.md) | -| `glance.publish`:L0/L1 source 卡片或 Splash script 卡片 | 已合入([#72](https://github.com/OctoSense-org/OctoSense/pull/72));获得 `glance` 权限的隔离应用可以发布([#86](https://github.com/OctoSense-org/OctoSense/pull/86))。`sys.digest` 有单独的运行时/宿主要求;[#87](https://github.com/OctoSense-org/OctoSense/pull/87) 是历史跟踪链接,不表示当前 PR 状态 | [`crates/shell/src/glance.rs`](../crates/shell/src/glance.rs) | -| 审批、首次使用同意、开发者模式 | 已实现:Shell 审批路由、常设规则及面板接收 AI 服务总线和内核工具审批;首次运行先征求同意,开发者模式由用户开启 | [架构 § 审批](architecture.zh-CN.md#5-审批) | -| 系统 Agent 的工具集 | 已强制:不提供 octos 内置 shell;精确设置系统会话内核工具列表。用户开启命令执行后通过 `terminal.run`,每次命令实时确认;Terminal 可用时模块和进程托管均支持 | [架构 § 工具与授权](architecture.zh-CN.md#4-工具与授权) | -| 原生应用只声明一次(`native-apps.json`),按目标平台决定托管方式;Terminal 作为系统应用在桌面端以独立进程运行 | 目前可用([#113](https://github.com/OctoSense-org/OctoSense/pull/113)) | [架构 § 原生应用](architecture.zh-CN.md#原生应用进程内还是独立进程) | -| 应用自己的 Agent:声明与执行 | News/Mail/Calendar/Photos/Maps/Camera/YouTube 的 `tools.json`、通用工具授权、peer 和宿主服务执行器已实现。`AGENT.md` 提示词加载、包内 skills/模型选择和自动触发器仍有缺口 | [见下文](#规划中事件驱动的应用-agentadr-0002) | -| 独立进程中的原生应用访问自己的 Agent(peer link) | Makepad 客户端与 Shell `crates/shell/src/peer_link/` 均已实现。当前没有进程应用申请自己的 Agent;Terminal 声明了工具但未申请 peer | [架构 § 应用与它自己的 Agent](architecture.zh-CN.md#应用与它自己的-agent) | - -## 架构 +## 各个组成部分 ```mermaid flowchart TB person(["用户"]) - subgraph shell["一个 OctoSense Shell 进程:桌面端,或手机上的 Home"] - providers["AI providers 应用(os.ai-providers)
隔离运行的脚本应用"] - sheet["宿主面板
密钥、QR、PIN"] - llm["llm 宿主服务
写入提供方配置"] - aihost["crates/ai-host
start、策略、offer"] - broker["crates/app-peers broker
每个获授权的应用/账号一个 peer"] - runner["Card runner(App Hub)
宿主服务:mail、news、glance、llm、model、octos"] - router["审批路由
crates/shell/src/approvals"] - relay["Shell 宿主工具 relay
声明和调用者检查"] - bus["AI 服务总线
crates/shell/src/ai_bus.rs"] - rinx["Rinx(原生模块)"] - appcard["AppCard(原生,需主动开启)"] - scripts["脚本应用(系统和商店)"] + provider(["AI 提供方"]) + subgraph shell["Shell 进程"] + providers["AI providers 应用"] + sheets["宿主面板
密钥、QR 码、PIN"] + scripts["脚本应用
在 Card runner 中"] + natives["原生模块
Rinx、Notes、Clock 等"] + subgraph aihost["crates/ai-host"] + llm["llm 服务"] + model["model 服务"] + octos["octos 服务"] + broker["app-peers 代理
每个应用、每个账号一个 peer"] + end + relay["宿主工具中转"] + router["审批路由"] + exec["工具执行者
宿主服务、工具箱、应用窗口"] + glance["glance 服务"] end - vault[("平台密钥存储
或仅所有者可读的配置")] - kernel[("octos 内核,每个 Shell 一个
配置 _main,系统 Agent 会话")] + term["Terminal
在系统沙箱中的进程应用"] + vault[("密钥")] + kernel[("octos 内核
系统 Agent、应用 peer")] person --> providers - person --> sheet + person --> sheets providers -- "llm.*" --> llm - sheet -- "llm.sheet.*" --> llm + sheets -- "llm.sheet.*" --> llm llm --> vault - llm -- "变更后重启" --> kernel - aihost --> kernel - aihost --> broker - broker -- "peer/prepare、peer/context/open、turn/start" --> kernel - rinx -- "受限的 OctosAppService" --> broker - appcard -- "自己的连接" --> kernel - scripts -- "host.request" --> runner - runner -- "octos.* 服务
broker 身份 card.<应用 id>" --> broker - rinx -- "类型化工具" --> bus - bus -- "confirm: host 调用" --> router - kernel -- "peer/tool/call" --> relay - relay --> runner - kernel -- "approval/requested" --> router - router --> person + llm -- "配置文件,重启" --> kernel + scripts -- "octos.*" --> octos + scripts -- "model.*" --> model + scripts -- "glance.*" --> glance + octos -- "card.<应用 id>" --> broker + natives --> broker + term -- "peer link" --> broker + broker -- "OUP" --> kernel + vault -.-> kernel + vault -.-> model + kernel --> provider + model --> provider + kernel -- "peer/tool/call、审批" --> relay + relay --> router + relay --> exec + exec -- "卡片" --> glance + router -- "面板" --> person ``` -### octos 内核:每个 Shell 一个 +[`crates/ai-host`](../crates/ai-host/README.md) 是两个 Shell 共同调用的唯一入口。启动时,`start(Host::platform(data_dir))` 配置内核、安装宿主策略,并注册 `llm` 和 `model` 服务;在托管了内核的 Shell 中还会注册 `octos` 服务。在原生模块的 `create` 前后,`offer` 和 `finish` 向新实例提供注入的助手服务,只有 Rinx 领取它。`shutdown` 停止内核。 -内核是一个 **Shell 服务**([`crates/kernel`](../crates/kernel/README.md),包名 `octosense-kernel`),不是应用。Shell 在启动时配置一次;在第一个使用者调用 `octosense_kernel::connect()` 之前什么都不运行,之后的使用者共享同一个进程。octos 对它的数据目录持有单写者锁,所以每个 core 目录只有一个内核。普通模式下最后一个连接关闭时内核停止;Talk to Octos 开启期间会保持共享内核存活;`shutdown()` 随 Shell 一起停止它(最多 5 秒)。 +在这张图之外,可选的 AppCard 原型自己打开内核连接;桌面端的 AI 面板(Makepad 的 `aichat`)则经由 Makepad 的 AI 服务总线调用应用的工具。 -各平台的运行方式(`crates/kernel/src/launch.rs`,`crates/ai-host` 中的 `KernelSource::platform()`): +### 内核服务 -| 平台 | 内核 | Core 目录(octos home) | +每个 Shell 运行一个 octos 内核,作为 [`crates/kernel`](../crates/kernel/README.zh-CN.md) 中的一项服务。第一个使用方连接时它才启动,内核从哪里来由 `launch.rs` 决定: + +| 平台 | 内核 | core 目录 | | --- | --- | --- | -| 桌面端(macOS;Windows 和 Linux 未测试) | `<内核> serve --stdio --data-dir `(存在 `/config.json` 时再加 `--config`),内核为显式指定的内核程序(`Options::program`),否则为 `$OCTOS_APP_CORE_BIN`,再否则为 Shell 旁随附的 `octos-kernel`;后者只有在收据中的版本与固定的 octos 版本一致且 SHA-256 相符时才运行([desktop/README.zh-CN.md,构建与运行](../desktop/README.zh-CN.md#构建与运行))。**三者都没有时没有桌面内核**,开发者自己运行的 `octos serve` 永远不会被动到。 | `$OCTOS_APP_CORE_DIR`,否则为 `/octos-home/.octos`(`~/.octosense/octos-home/.octos`):OctoSense 自己的目录,不再是用户的 `~/octos-home/.octos`(只从中复制一次提供商设置) | -| Android(Home) | APK 中的 `liboctos.so serve --stdio`,由 [`tools/kernel-artifact.py`](../tools/kernel-artifact.py) 按根 `Cargo.toml` 锁定的 octos 版本构建 | `<应用数据目录>/octos-home/.octos` | -| OpenHarmony | 进程内运行(`octos_cli::embedded::serve_io`),因为 HAP 不能 exec | `<应用数据目录>/octos-home/.octos` | -| iOS | **没有。** 提供方仍会保存;没有应用能获得助手 | – | +| 桌面端 | 子进程(`serve --stdio`):`OCTOS_APP_CORE_BIN` 指定的二进制;否则是 Shell 旁随附、回执中的版本与固定版本一致的 `octos-kernel`;都没有就没有内核 | `~/.octosense/octos-home/.octos` | +| Android | 子进程:APK 中的 `liboctos.so` | `<应用数据目录>/octos-home/.octos` | +| OpenHarmony | 在 Shell 进程内(`octos_cli::embedded::serve_io`) | `<应用数据目录>/octos-home/.octos` | +| iOS | 没有:提供方会被保存,但没有应用能获得助手 | – | -开启 Talk to Octos 后,内核使用宿主管理的回环 WebSocket 代替 stdio。使用者通过 `Connection` 使用 octos UI Protocol(JSON-RPC 帧,与 `octos serve --stdio` 相同)。每个使用者只收到自己请求的回复和自己会话的通知。提供方变更时内核重启,所有连接以 `CloseReason::Restarted` 结束;使用者重新连接并重新打开会话(AppCard 的传输层是参考实现)。 +桌面端只会从用户自己的 `~/octos-home/.octos` 复制一次提供方设置,从不往那里写入。重启与空闲停止见 [architecture.zh-CN.md 第 1 节](architecture.zh-CN.md#octos-内核)。 ### AI providers 与 `llm` 宿主服务 -**AI providers**(`os.ai-providers`,[`apps/ai-providers/bundle`](../apps/ai-providers/bundle))是用户选择助手所用模型的地方:一个主模型和若干备用模型,都来自 octos 的模型目录,可以测试连接,并可通过有 PIN 保护的 `OCTOS1E` QR 码在设备之间迁移。桌面端从 **Start → Settings → AI providers** 打开,手机上从 **OctoSense Settings → Accounts → AI providers** 打开。 - -它和其他应用一样是隔离运行的脚本应用;有特权的一半是 **`llm` 宿主服务**([`apps/ai-providers/host-service`](../apps/ai-providers/host-service/README.md)): - -- 它写入内核的配置 `/profiles/_main.json`(`config.llm` 和 `config.env_vars` 中的密钥变量),然后重启内核。 -- 密钥存入 macOS 登录钥匙串(服务名 `octos`);桌面 Linux 上存入 `/secrets/`(0600);Android、iOS 和 HarmonyOS 上存入应用私有的配置文件(0600):octos 在 Android 和 iOS 上没有密钥存储,而 HarmonyOS 上内嵌的内核会到 Shell 自己的 `HOME` 下找密钥存储。开发时可用 `OCTOSENSE_LLM_VAULT=file` 把密钥留在配置文件中。 -- 密钥、PIN 和 QR 码只在服务自己的**宿主面板**上输入、绘制和扫描。只有面板能调用 `llm.sheet.*`;应用只看到掩码后的状态(`"set ••••1234"`、`"missing"`)。 -- 它**只服务 `os.*` 应用**(`"llm is for OctoSense's own apps."`)。它管理提供方;没有任何向模型发送提示词的方法。 +**AI providers**(`os.ai-providers`,[`apps/ai-providers/bundle`](../apps/ai-providers/bundle))是用户选择助手所用模型的地方:从 octos 的模型目录中选一个主模型和若干备用模型。有 PIN 保护的 `OCTOS1E` QR 码可以把它们迁移到另一台设备。这个应用是普通的脚本应用,没有 Agent。它的特权部分是 **`llm` 宿主服务**([`apps/ai-providers/host-service`](../apps/ai-providers/host-service/README.md)): -### `crates/ai-host`:Shell 的统一入口 +- 它写入内核的配置文件 `/profiles/_main.json`,然后重启内核。 +- 密钥、PIN 和 QR 码只出现在它的宿主面板上。只有面板能调用 `llm.sheet.*`;应用只看到掩码后的状态,例如 `"set ••••1234"`。 +- 它只服务系统应用(`os.*`),也没有任何接收提示词的方法:`llm.test` 只发送固定的 “ping”。 -[`crates/ai-host`](../crates/ai-host/README.md)(`octosense-ai-host`)是两个 Shell 都调用的入口:启动时调用 `start(Host::platform(data_dir))`(配置内核、安装宿主策略、注册带平台 QR 导入能力的 `llm`),每个事件调用 `handle_event`,在原生模块 `create` 前后调用 `offer(module, scope)` / `finish()`,退出时调用 `shutdown()`。 +密钥存放在内核读取它们的地方(`vault.rs`):macOS 上在登录钥匙串中(服务名 `octos`),桌面 Linux 上在 `/secrets/`(权限 0600),Windows、Android、iOS 和 OpenHarmony 上则直接写在配置文件里(其中类 Unix 系统上权限为 0600)。`OCTOSENSE_LLM_VAULT=file` 让所有平台都把密钥留在配置文件中,[本地运行](#本地运行与测试)就是这样做的。 -宿主**策略**从 `native-apps.json` 生成的声明读取原生 `octos.*` 精确授权(目前是 Rinx)。 - -#### 隔离应用的首次使用同意 - -隔离应用默认需要用户在首次使用时同意。环境变量 `OCTOSENSE_CONTAINED_APPS` 控制这项策略: +## 信任模型 -| 设置 | 行为 | +| 规则 | 代码如何保证 | | --- | --- | -| 未设置(默认) | 应用首次使用自己的 Agent 前,先询问用户(`ContainedGate::Consent`)。 | -| `1` | 开发者覆盖设置:跳过首次使用同意。已声明服务的限制和每次工具调用的审批规则仍然有效。 | -| `0` | 为隔离应用关闭 `octos` 服务。 | +| **密钥留在 Shell。** | 只有 `llm` 服务写入密钥;内核和 `model` 服务读取它们。没有哪个 `octos.*` 调用携带密钥,app-peers 约定中也没有存放密钥的字段(`ModelInfo`)。 | +| **机密只在宿主面板上输入。** | App Hub 只接受来自宿主面板的 `.sheet.*` 调用,从不接受应用发来的。脚本应用中的密码输入框不起作用,App Hub 的准入检查也会拒绝声明了此类输入框的应用包。 | +| **应用从不使用内核协议。** | 应用只能经由 app-peers 代理访问自己的 Agent,代理在每次调用上标注应用的身份。 | +| **最小权限,按精确名称。** | 应用得到的 `octos.*` 服务,必须是它声明了、确实存在、并且宿主授予了的。`octos.` 或 `octos.admin` 不授予任何权限(`crates/app-peers/src/contract.rs`、`hosted.rs`)。 | +| **审批属于用户。** | 应用的 peer 提出的每一次审批都交给 Shell 的审批路由;应用只收到 `approval/handled_by_host`。系统 Agent 不能批准。开发者模式只能由用户打开,它会批准所覆盖应用经路由的调用([判定顺序](architecture.zh-CN.md#5-审批))。 | +| **邮件只有在用户确认确切内容后才会发出。** | 没有哪个 Agent 工具能发送邮件,`mail.send` 也会拒绝。只有在宿主的审阅界面(展示确切的发件人、收件人、主题和正文)上亲手触摸 Approve & Send,才能授权发送;开发者模式和常设规则都不能。目前只有 Android 触摸屏能这样确认(`mail_review.rs`,[可组合的邮件卡片](mail-composable-cards.zh-CN.md))。 | +| **每一轮都带着来源。** | Shell 把这一轮由谁发起标注在它的工具调用和审批上([见下文](#调用))。 | +| **记忆和文件属于一个应用、一个账号。** | 每个 peer 有自己的记忆命名空间 `app//acct-`,最多只能访问自己账号的文件夹。 | -首次使用时,Shell 会说明应用的 Agent 可以读取和使用什么,以及模型在哪里运行。**设置 → 助手 → 审批**列出每个应用的 Agent,用户可以在此关闭它。 +工具如何声明、授予和中转,见 [architecture.zh-CN.md 第 4 节](architecture.zh-CN.md#4-工具与授权)。 -策略由 [`crates/ai-host/src/lib.rs`](../crates/ai-host/src/lib.rs) 中的 `contained_gate_from` 选择。每个应用的同意状态由 [`crates/shell/src/approvals/consent.rs`](../crates/shell/src/approvals/consent.rs) 处理([#120](https://github.com/OctoSense-org/OctoSense/pull/120));详见[架构 § 审批](architecture.zh-CN.md#5-审批)。 - -### `crates/app-peers`:宿主拥有的应用 peer - -[`crates/app-peers`](../crates/app-peers/README.md) 是应用和内核之间的中介(Rinx [ADR 0007](https://github.com/hagency-org/Rinx/blob/main/docs/adr/0007-host-owned-octos-app-peers.md);内核一侧是 octos UPCR-2026-034): +## 各类应用目前能用什么 -- 原生模块声明的 `octos.*` 服务获得策略授权后,该应用会在 Shell 的内核上得到**每个账号一个 octos peer**,通过 `peer/prepare` 创建或恢复。没有获得任何授权的模块不会分配 peer。 -- peer 的**所有者**是 Shell 的系统 Agent 会话 `_main:api:octosense#system`。内核为 peer 生成宿主 token;Shell 把它保存在 `/../app-peers`(0600),任何应用都无法访问。 -- peer 的**记忆命名空间**是 `app//acct-`:按应用和应用账号区分(hash 是账号 id 的非机密标签)。Shell 选择获准的账号工作目录,内核创建并核验。不支持这一约定的内核会被拒绝,绝不会退回到使用配置共享记忆的普通会话。 -- 应用永远看不到内核协议。它为正在创建的实例拿到一个受限的 `OctosAppService`,绑定已登录的账号,并为每个客户端实例打开一个**请求上下文**(`peer/context/open`)。请求上下文是同一 peer 下独立的内核 session,不是另一个应用 peer。`open_conversation` 创建带有限共享历史的人类通道,peer session 处理系统委派。切换账号会撤销旧账号的所有上下文;迟到的事件会被丢弃。 -- 操作包括 `Open`、`History`、`Turn { text }`、`Interrupt` 和 `Approval { id, approve }`,每个都受对应的精确服务名约束(`octos.session.open`、`octos.session.history`、`octos.turn.start`、`octos.turn.interrupt`;审批需要 `octos.turn.start`)。broker 默认回合超时为 180 秒。 +有四类应用拥有 Agent:通过注入服务的 Rinx;通过 peer link 的其他带 Agent 的原生应用(Calculator、Clock、Notes、Reminders、Weather 和 Terminal,见 [architecture.zh-CN.md](architecture.zh-CN.md#应用与它自己的-agent));除 AI providers 以外的系统脚本应用;以及商店脚本应用。**部分可用**表示在所注明的限制内可用;**–** 表示不适用。 -### 系统 Agent +| | Rinx | 其他原生应用 | 系统脚本应用 | 商店脚本应用 | +| --- | --- | --- | --- | --- | +| 用户允许后拥有 Agent | 可用,在 Rinx 打开且已登录期间 | 可用,在应用打开期间 | 可用,获准后即准备,此后每次启动时也会准备 | 可用,前提是应用包声明了 Agent(`octos.*`、`agent` 块或 `tools.json`);获准后即准备,此后每次启动时也会准备 | +| 应用自己的界面与 Agent 对话 | 部分可用:`OctosAppService` 只用于小程序的私有上下文;用户在 “Ask Rinx” 面板中对话 | 可用:`OctosPeer`,但随附的应用只用它提供工具 | –(都没有声明 `octos.*`) | 可用:[`octos` 服务](#脚本应用与-octos-服务) | +| Agent 使用应用自己的工具 | 尚未支持:它的工具只服务于 AI 面板 | 可用:只读工具,在已打开的窗口中运行 | 可用:在应用的宿主服务或 Shell 的通知服务上运行 | 尚未支持:没有宿主服务或应用执行器来运行它们 | +| 每一轮都附带 `AGENT.md` 和技能 | – | – | 可用(只有邮件附带了它们) | 可用 | +| 由事件启动 Agent | 尚未支持 | 尚未支持 | 部分可用:只有邮件的新邮件触发器 | 尚未支持 | +| glance 屏幕上的卡片 | 尚未支持 | 尚未支持 | 可用,需 `glance` 权限;邮件的卡片可以带回复草稿 | 可用,需 `glance` 权限 | +| 就自己的卡片与 Agent 对话 | 尚未支持 | 尚未支持 | 可用:Card / Chat(邮件回复为 Email / Chat) | 可用:Card / Chat | +| 一次性模型调用 | – | – | 可用,需 `model` 权限(相册在用) | 可用,需 `model` 权限 | +| 系统工具箱 | 未使用¹ | 未使用¹ | 未使用¹ | 尚未支持 | +| 为 Agent 选择模型 | 尚未支持² | 尚未支持² | 尚未支持² | 尚未支持² | -系统 Agent 拥有应用 peer,通过 `peer_send_input` 委派任务,再用 `peer_gather` 获取结果。`agents.list` 发现应用 Agent;`agents.ask` 请求首次同意并准备 peer,本身不发送委派任务。 +¹ 只在带 `toolbox-peers` feature 的构建中提供,而且目前还没有应用声明 `research` 或 `crawl`。 -用户可用 F8 或 Shell 的 Assistant 入口打开系统聊天(`crates/shell/src/system_chat/`),也可通过桌面 “Ask <app>” 面板或卡片 `sys.chat` 直接与应用 Agent 对话;手机尚无打开 Ask-app 面板的触控入口。 +² 每个 Agent 都使用 AI providers 中设置的提供方;Shell 不读取 manifest 的 `model.needs`。 -系统 Agent 使用受限内核工具列表和审核过的 Shell 工具(例如需开启的 `terminal.run`),不会继承所有应用工具,也不能替用户审批。Talk to Octos 是另一种客户端入口。自动触发调度、学习 overlay 和更丰富的 glance 排序仍在规划中;卡片目前按优先级和时间排序。 +内核工具要授予,不会继承:Rinx 的 Agent 保留 octos 的文件、记忆和网页工具,脚本应用的 Agent 只保留 `ask_user_question`(App Hub 唯一准入的内核工具),其他原生应用的 Agent 一个也没有。 -### Shell 中的其他助手 +没有哪个 Agent 会自行在设备之外采取行动。邮件的 Agent 会起草回复(`mail.propose_reply`、`mail.suggest_reply`)并提议发送(`mail.propose_send`),但它没有发送工具:由用户在宿主的审阅界面上发送([信任模型](#信任模型))。 -- **AppCard**([`apps/appcard`](../apps/appcard)),即 "Ask anything" 助手,是一个使用自己的内核连接和会话的原生模块。它需要主动开启(`--features app-appcard`),不随产品发布。 -- 桌面端的 **AI 面板**承载 Makepad 自己的 `aichat` 应用,它通过窗口管理器的 AI 服务总线(`crates/shell/src/ai_bus.rs`)调用应用的类型化工具。Rinx 的助手工具目前就是这样被调用的(见下文)。它与 octos 应用 peer 是分开的。 +## 脚本应用与 `octos` 服务 -## 信任模型 +脚本应用通过 `octos` 宿主服务的四个调用与自己的 Agent 对话,这个服务由 `ContainedOctos` 提供([`crates/ai-host/src/contained.rs`](../crates/ai-host/src/contained.rs))。只要 Shell 托管了内核就会注册它,即使同意闸门关闭也照样注册,好让应用得知自己为何被拒绝。一个应用的所有调用都交给同一个 peer `card.<应用 id>`:它代表 `device`,在保存账号的应用(如邮件)中则代表已登录的账号。“Ask <app>” 面板和系统 Agent 使用的也是这个 peer。 -| 规则 | 目前如何保证 | -| --- | --- | -| **密钥留在宿主。** | 只有 `llm` 服务读写密钥;密钥保存在平台密钥存储或内核 core 目录下仅所有者可读的配置中,从不进入应用的 jail。app-peers 约定中不携带任何凭据(`ModelInfo` 中没有)。没有任何 `octos.*` 服务允许应用选择提供方或提交密钥。 | -| **机密归宿主所有。** | 应用不收集密码、PIN、密钥或一次性验证码,即使只是转交也不行。运行时让受管控隔离环境中的密码输入框失效,App Hub 的准入检查拒绝声明了此类输入框的应用包,服务在自己的面板上询问(`.sheet.*` 只接受来自面板的调用)。 | -| **审批属于用户。** | Shell 将 `host_tool` 以及其他内核工具审批交给宿主路由,应用只收到 `approval/handled_by_host`。`confirm: app` 使用明确注册的所属应用面板。系统 Agent 不能代替用户同意;常设规则和开发者模式由用户控制,仍受工具限制,例如 Terminal 命令不能自动批准。见[审批](architecture.zh-CN.md#5-审批)。 | -| **委派和工具执行的权限不同。** | 系统 Agent 委派给应用 peer;relay 检查授权、schema 和策略后以所有者身份执行工具。跨应用工具授权必须明确,不是获得所有 API 或数据库;系统会话另有审核过的 Shell 工具,如 `terminal.run`。 | -| **记忆按应用和账号私有。** | 每个 peer 的记忆命名空间是 `app//acct-`;一个账号的上下文从不在另一个账号下恢复。提升到共享记忆还在规划中(ADR 0002 §9)。 | -| **最小权限,按精确名称。** | 应用得到的是 `声明 ∩ 支持 ∩ 宿主策略` 的服务,按精确名称比较:`octos.` 或 `octos.admin` 不授予任何权限。 | +### 同意闸门 -## 各类应用目前能用什么 +`Policy::contained_gate`([`crates/ai-host/src/lib.rs`](../crates/ai-host/src/lib.rs))这道闸门决定一次调用能否到达 peer。默认是 `Consent`:用户还没有做出决定的应用第一次调用时,Shell 显示首次使用面板([`approvals/consent.rs`](../crates/shell/src/approvals/consent.rs)),说明 Agent 可以读取和使用什么、模型在哪里运行。用户允许之前,调用都会被拒绝。用户的答复保存在 OctoSense home 的 `approvals/consent.json` 中。`OCTOSENSE_CONTAINED_APPS=1`(`Everyone`)在开发时跳过这个面板,`0`(`Off`)则拒绝所有调用。 -### 原生模块(Rinx 这类) +设置中列出每个应用的 Agent,各带一个关闭开关。关闭后,它的 peer 立即被释放;被拒绝的应用会一直保持拒绝,直到用户在那里重新打开它。开发者模式会为它覆盖的应用跳过这个面板。 -原生模块是链接进 Shell 的受信任 Rust 代码。**Rinx**([hagency-org/Rinx](https://github.com/hagency-org/Rinx))是参考实现,也是 `Policy::shipped()` 下唯一获得助手授权的模块。 +### 调用 -1. 模块在 `capabilities()` 中列出精确的服务名,例如 `octosense_app_peers::OCTOS_SERVICES` 中的全部四个。 -2. Shell 在 `module.create` 之前调用 `ai_host::offer(module, &scope)`,之后调用 `offer.finish()`;返回的 `Assistant` 与实例同生命周期,被丢弃时释放。 -3. 在 `create` 中,模块领取自己的服务;`None` 表示以无助手的方式托管,模块不得退回到自己启动的内核: +应用只能调用 manifest 中声明了的 `octos.*` 名称:Card runner 的隔离环境会拒绝其余的调用,服务还会再检查一次(`contained::declared`)。这些调用作用于应用的对话,也就是它的 peer 上用户的通道([一个应用 Agent,两条通道](../README.zh-CN.md#一个应用-agent两条通道)): - ```rust - // Option>;None:此实例没有助手。 - let Some(service) = octosense_app_peers::injection::claim("rinx", &handles.scope.to_string()) else { return }; - service.set_account(Some(&user_id)); - let ctx = service.open_context(ContextSpec { account, instance, services })?; - ctx.call(ContextOp::Turn { text }, sink)?; // Data(..)*,然后 Complete(..) - ctx.close(); // 实例关闭 - service.release(); // 应用关闭 - ``` +| 调用 | 参数 | 返回(`r.data`) | +| --- | --- | --- | +| `octos.session.open` | `{}` | `{open: true, model, …}`;`model` 给出提供方和模型的名称,从不包含密钥 | +| `octos.session.history` | `{}` | 对话的 `messages`:两条通道按时间合并,每条消息都带有 `lane` 和说话者 | +| `octos.turn.start` | `{text, trigger?, from?}`,`text` 最多 32 KiB | 这一轮结束后返回 `{turn_id, text, speaker, lane}` | +| `octos.turn.interrupt` | `{}` | `{interrupted, turns}`:两条通道上正在运行的回合都会停止 | -4. `availability()` 报告 `Unavailable`(没有内核、未授权、未登录)、`Idle`、`Ready` 或 `Failed`;在任何状态下应用的普通界面都照常工作。`settings_entry()` 为 `Host`:应用不提供自己的提供方表单,而是引导用户去 AI providers。 +`trigger` 说明这一轮由什么发起:`person`(应用声称是用户发起的)、`app`、`schedule` 或 `background`(应用自己的运行),或者带 `from` 的 `incoming`(别人发来的内容)。不填时,这一轮记为 `unknown`,最不受信任(`crates/app-peers/src/contract.rs` 中的 `TurnTrigger`)。对话记录会把 `person` 的回合标为用户的话,但审批规则把它当作应用自己的运行:只有 Shell 的 “Ask <app>” 面板能为用户作证。卡片里的对话虽然由 Shell 绘制,也算作应用自己的运行。常设规则会跳过 `incoming` 和 `unknown` 的运行,除非某条规则明确选择包含它们。 -**工具。** Rinx 在 `src/assistant/mod.rs` 定义其 UI 助手工具,通过 AI 服务总线上的 `ServiceExecutor` 执行,读取和发送仍走自己的确认界面。内核宿主注册工具及 Shell relay 已实现,缺口不是等待 octos#2567 合并:当前 Rinx 原生清单未声明这些 peer 工具,也尚未通过 `OctosAppService::set_confirm_sheet` 将发送确认面板交给 Shell。因此不要把总线上能调用的工具当作 Rinx peer 已拥有的工具。 +每个应用同一时间只运行一轮,一轮超过 180 秒会被代理中断。脚本应用收不到推送的事件,所以它读取 `octos.session.history`,其中也包含系统 Agent 的回合。没有任何参数能携带审批决定。Design Flow 的指南中有[最小调用示例](https://github.com/OctoSense-org/OctoScript-App-Design-Flow/blob/main/docs/AI-SERVICES.zh-CN.md#最小调用示例与不可用状态)。 -**Rinx 迷你应用。** Rinx 托管经过审核的 OctoScript 迷你应用,并向它们提供同样的四个 `octos.*` 服务,每个运行中的实例使用 Rinx peer 的一个独立请求上下文([示例](https://github.com/hagency-org/Rinx/tree/main/examples/miniapps/matrix-octos-script))。这是 Rinx 自己的迷你应用宿主,适用于用户审核后导入 Rinx 的应用包;它不是 App Hub 的安装路径。 +### 错误 -### 隔离运行的脚本应用(系统应用和商店应用) +| `r.error` | 原因 | +| --- | --- | +| `this app was not granted "octos", which "" needs` | manifest 中没有这个名称;隔离环境立即返回。 | +| `no service answers "octos" on this device` | Shell 没有托管内核(iOS)。 | +| `Waiting for the person to allow this app's agent (OctoSense asks the first time)` | 用户尚未同意,或已拒绝。 | +| `The assistant is turned off for apps on this device` | 闸门为 `Off`。 | +| `Unsupported Octos arguments`、`Provide text (at most 32 KiB)` | 参数超出上表的规则。 | +| `Add an account in the app before using its assistant` | 应用保存账号,但还没有账号。 | +| `This app already has an assistant turn running` | 上一轮还在运行时又发起了一轮。 | +| `no octos kernel: …` | 桌面端没有内核二进制([见下文](#本地运行与测试))。 | -脚本应用在 App Hub 的 Card runner 中运行,只能通过 `host.request(".", args, fn(r){…})` 访问 Shell,而且 family 必须是 manifest 已获授予的权限。Shell 目前注册的服务(`crates/shell/src/apps.rs`、`crates/ai-host`): +Design Flow 的[错误](https://github.com/OctoSense-org/OctoScript-App-Design-Flow/blob/main/docs/AI-SERVICES.zh-CN.md#错误)一表说明了应用对每种情况应当显示什么。 -| Family | 谁可以调用 | 是什么 | -| --- | --- | --- | -| `mail` | 获得 `mail` 授权的任何应用 | 用户在宿主面板上登录的邮件账号 | -| `calendar` | Calendar Agent 以 `os.calendar` 身份调用;无公开脚本能力 | Rust 日程存储和固定 glance 卡片工具;脚本窗口目前只是说明,不能通过通用 Calendar 能力调用这个 family。 | -| `llm` | 仅 `os.*` 应用 | 管理助手的提供方(`llm.providers`、`llm.add_provider`、`llm.test`、`llm.import_qr` 等);**没有提示词或补全方法** | -| `news` | 仅 `os.*` 应用(服务的 `may_call`) | 不使用模型的 News 数据服务。News bundle 已声明 `news`,其 Agent 暴露 `news.list`/`news.read`/`news.notify`;后台抓取计时器尚不会启动 LLM 回合。 | -| `glance` | 获得 `glance` 权限的应用([#86](https://github.com/OctoSense-org/OctoSense/pull/86)、[App-Hub#22](https://github.com/OctoSense-org/OctoSense-App-Hub/pull/22)) | 以应用自己的 id 向 glance 屏幕发布 L0 卡片(`crates/shell/src/glance.rs`) | -| `model` | 获得 `model` 权限的应用([App-Hub#24](https://github.com/OctoSense-org/OctoSense-App-Hub/pull/24)) | 一次性的 `model.complete {task, input, schema, class?, allow_urls?}` 和 `model.budget`([#95](https://github.com/OctoSense-org/OctoSense/pull/95),`apps/ai-providers/host-service/src/complete/`,在 `crates/ai-host/src/lib.rs` 中注册):`class` 为 `fast` 或 `strong`;宿主从用户的提供方中挑选模型,按 schema 校验回复,除非请求否则拒绝含 URL 的回复,按应用管理每日预算;没有工具、记忆和历史,应用也看不到任何密钥 | -| `octos` | 声明了确切 `octos.*` 服务名的应用;需要 Shell 托管内核,并遵守[隔离应用的首次使用同意](#隔离应用的首次使用同意)策略 | 应用自己的助手,经由宿主拥有的 peer 访问。broker 用 `card.<应用 id>` 标识应用;`peer/prepare` 返回内核分配的 peer slug([`contained.rs`](../crates/ai-host/src/contained.rs),[#106](https://github.com/OctoSense-org/OctoSense/pull/106))。调用规则见下文。 | +## 一次性模型调用:`model` 服务 -四个 `octos.*` 调用的参数、来源、审批和回复各有规则: +有些工作只需要一个有边界的回答,而不是一个 Agent;相册就用它把照片归成回忆。获得 `model` 权限的应用调用 `model.complete {task, input, schema, class?, allow_urls?}`,或者用 `model.budget` 查看自己的预算([`complete/`](../apps/ai-providers/host-service/src/complete/mod.rs)): -- **参数。** `octos.turn.start` 必须提供非空白、最多 32 KiB 的 `text`,还可以提供 `trigger`(`person`、`app` 或 `incoming`)和 `from`。其余调用只接受 `{}`。 -- **来源。** `trigger: "person"` 会得到 `AppSaysPerson` 分类。它只是应用的声明,不是受信任的人类操作,也不能绕过审批。 -- **审批。** 宿主工具审批和其他 peer 审批都交给 Shell;工具仍受已有授权与审批规则约束,开发者模式会为它覆盖的应用作答。没有审批路由的宿主会拒绝请求,并在 `denied_approvals` 中列出。详见 [`host_approvals.rs`](../crates/app-peers/src/host_approvals.rs)。 -- **回复。** 宿主拒绝超过 2 MiB 的回复。下表列出成功调用及常见错误的返回内容。 +- 它绕过内核:服务读取同一份配置文件和密钥,自己调用提供方。模型只看到固定的指令、任务、schema 和输入。 +- `class` 为 `fast`(默认)或 `strong`。宿主按用户设定的顺序尝试提供方,同类的优先。应用只知道回答的是哪一类,从不知道提供方、模型或密钥。 +- 回复必须是能通过 `schema` 校验的 JSON,最多 16 KiB,而且除非设置了 `allow_urls`,不能含有 URL。不合格的回复会重试一次,仍不合格就拒绝。 +- 每个应用的预算默认为每分钟 6 次调用,每个 UTC 日 100 次调用和 100,000 个 token,记录在 `/.host/model/ledger.json` 中,位于所有应用的 jail 之外。 +- 拒绝的形式是 `: `,`code` 为 `capability`、`no_provider`、`rate`、`budget`、`bad_request`、`invalid_output`、`too_large` 或 `provider` 之一。 -脚本自己的助手调用使用已声明的 `octos.*`;Shell 也可以根据 `agent`/`tools.json` 驱动 peer,而不要求脚本声明这些 UI 调用。脚本服务调用的回复: +Design Flow 的[一次性模型调用](https://github.com/OctoSense-org/OctoScript-App-Design-Flow/blob/main/docs/AI-SERVICES.zh-CN.md#一次性模型调用model)给出了调用示例。 -| 调用 | 返回 | -| --- | --- | -| manifest 未授予的 family | `r.error`:`this app was not granted "", which "" needs`(由隔离环境立即返回) | -| 已获授权的 `octos.turn.start {text}`,内核与提供方已配置 | `r.data`:`{turn_id, text}`,即应用 peer 的回复 | -| 参数超出规则的 `octos.*` | `r.error`:`Unsupported Octos arguments` 或 `Provide text (at most 32 KiB)` | -| 设备为应用关闭了助手时(`OCTOSENSE_CONTAINED_APPS=0`)的 `octos.*` | `r.error`:`The assistant is turned off for apps on this device` | -| 用户尚未允许该应用的 Agent 时的 `octos.*` | `r.error`:`Waiting for the person to allow this app's agent (OctoSense asks the first time)`,同时 Shell 显示首次使用面板(在 `contained.rs` 和 `approvals/mod.rs` 中读到;未在运行的 Shell 中验证,**unverified**) | -| 在既无随附内核又未设置 `OCTOS_APP_CORE_BIN` 的桌面端调用 `octos.*` | `r.error`:`no octos kernel: no packaged octos-kernel beside <目录> and no OCTOS_APP_CORE_BIN override; …`(随附内核版本过旧时:`no octos kernel: refusing the packaged kernel …`) | -| 在不链接内核的构建(iOS)中调用 `octos.*` | `r.error`:`no service answers "octos" on this device`(**未验证**) | -| 获得 `llm` 授权的商店应用调用 `llm.*` | `llm is for OctoSense's own apps.` | -| 在 App Hub 的 `card-host` 中调用任何服务 | `no service answers "" on this device`(`card-host` 不注册任何服务) | +## 系统工具箱 -**历史验证。** 第一行、`llm` 一行和 `card-host` 一行已于 2026-09-27 在 `card-host`(App Hub `362d832`)中运行验证。回复、参数不合规的返回和缺少内核的返回已于 2026-09-28 在 macOS 上运行验证:release 版桌面端、隐藏窗口,经启动器打开一个声明了 `octos.session.open` 和 `octos.turn.start` 的系统应用;使用按当时锁定的 octos `7bec0918` 构建的内核和用户自己的提供方时,回复来自 peer `card.<应用 id>`,内核数据中出现了它的记忆命名空间 `app/card.<应用 id>/…`。文本长度和开关关闭的返回由 `cargo test -p octosense-ai-host` 覆盖,走的是同一个分发函数。OctoScript-App-Design-Flow 的 [AI-SERVICES](https://github.com/OctoSense-org/OctoScript-App-Design-Flow/blob/main/docs/AI-SERVICES.zh-CN.md) 给出了示例应用,以及 Rinx 提供的四个 `octos.*` 调用的参数和返回形状。 +系统工具箱([`crates/toolbox`](../crates/toolbox/README.md))让应用 Agent 做有边界的调研,而不是自己操作浏览器:它提供固定的 OctoScript 工作流模板,以及搜索、读取网页和抓取的工具,Shell 把它们作为归属于 `toolbox` 的宿主工具提供([`toolbox_peers.rs`](../crates/ai-host/src/toolbox_peers.rs))。 -manifest 的 `agent` 和已准入 `tools.json` 已参与 Shell 的 Agent 发现、工具加载与授权;News/Mail/Calendar/Photos/Maps/Camera/YouTube 已有宿主服务实现。`profile`/模型需求、`AGENT.md` 提示词、包内 skills 和自动触发器尚未完整消费,`implemented_by: "app"` 没有脚本执行器。Mail 当前只声明 `mail.notify`,不包含 UI 的读信/发信方法;Agent 工作目录不会自动暴露宿主数据库。Rinx 小程序准入是另一套约定,不能默认它支持 App Hub 的所有元数据。 +- **各项权限授予什么。** `research` 授予 `workflow.run`、`workflow.fork`、`toolbox.search` 和 `toolbox.web_read`;`crawl` 授予 `toolbox.deep_crawl`。 +- **何时提供。** 只有带 `toolbox-peers` feature 的构建才有(手机端的默认构建有,桌面端没有),而且要在用户同意之后;脚本应用中目前只提供给系统应用。系统 Agent 一个也拿不到。 +- **预算与结果。** 模板经由 `model` 服务调用模型,因此与应用共用预算;结果写入 `/.host/toolbox/`,应用无法伪造。 -## 规划中:事件驱动的应用 Agent(ADR 0002) +## glance 卡片 -[ADR 0002](adr/0002-event-driven-app-agents.md)(状态 **Proposed**)描述事件驱动应用 Agent 的完整计划。应用 peer、工具执行和用户对话已经可用。自动触发器等剩余部分在下表单独列出;ADR 仍处于提案状态,并不表示所有组件都尚未实现。 +拥有 `glance` 权限的应用通过 `glance` 服务([`crates/shell/src/glance.rs`](../crates/shell/src/glance.rs))把卡片发布到 glance 面板(桌面端)或 glance 页面(手机端):`glance.publish`、`glance.withdraw` 和 `glance.list`。Shell 从调用方取得发布者,从不读取参数中的发布者;它把卡片绑定到发布时的账号,并限制每个应用每分钟最多发布 6 次、最多保留 4 张卡片。系统应用的 Agent 通过自己的工具发布卡片:`.notify` 用模型写的文字填充固定的卡片模板;邮件的 `mail.publish_card` 检查模型编写的卡片,并在提供 `draft_id` 时把它绑定到宿主保存的回复草稿。 -| 组成部分 | ADR | 状态 | -| --- | --- | --- | -| `tools.json`:名称、输入输出 schema、风险、确认、共享和实现 | §4、§12 | `host_tools/script_apps.rs` 已加载、注册到 peer 并转发到宿主服务执行器;News/Mail/Calendar/Photos/Maps/Camera/YouTube 已声明工具。任意 `implemented_by: "app"` 脚本工具执行仍不可用。 | -| `AGENT.md`、只含数据的 skills、模型需求、`background` 和触发器 | §2、§3 | 已准入的元数据;Shell 尚未实现提示词/skill 加载、按这些需求选模型和触发调度。工具可后台运行不等于会自动启动回合。 | -| 内核:宿主注册工具、风险/列表检查、宿主审批、通用工具白名单和 `peer/input` | §4、§13 | 已实现并在锁定版本中;Shell 注册工具、接收输入、检查 schema/预算并通过 relay 调用执行器。见 `crates/app-peers` 和 `crates/shell/src/host_tools/`。 | -| 工具审批策略 | §4、§12 | 破坏性和对外调用进入 Shell 的审批路由,由它应用常设规则、宿主或已注册的应用确认界面,以及开发者模式。授权、schema 和调用方预算另行检查。详见[审批](architecture.zh-CN.md#5-审批)。 | -| News M1:`news` 数据服务(不使用模型) | 第一个切片 | 已合入([#69](https://github.com/OctoSense-org/OctoSense/pull/69));issue [#60](https://github.com/OctoSense-org/OctoSense/issues/60)。 | -| News M2:`os.news` peer 及工具 | 第一个切片 | 已实现:manifest `agent`、`news.list`/`news.read`/`news.notify`、Shell peer 准备与 relay。 | -| News M3:触发器和无人值守运行 | 第一个切片 | 规划中:[#62](https://github.com/OctoSense-org/OctoSense/issues/62)。 | -| News M4:glance 卡片(`glance.publish`、glance 页面和桌面面板) | §7、§8 | `glance.publish`([#72](https://github.com/OctoSense-org/OctoSense/pull/72))和供隔离应用使用的 `glance` 权限([#86](https://github.com/OctoSense-org/OctoSense/pull/86),配合 [App-Hub#22](https://github.com/OctoSense-org/OctoSense-App-Hub/pull/22))已合入;`sys.digest` 有单独的运行时/宿主要求;[#87](https://github.com/OctoSense-org/OctoSense/pull/87) 是历史跟踪链接,不表示当前 PR 状态;issue [#63](https://github.com/OctoSense-org/OctoSense/issues/63)。 | -| M5:作为获授权工具的系统工具箱 | §6 | 已在 `toolbox-peers` feature 下实现,见 `crates/ai-host/src/toolbox_peers.rs` 和 Shell 工具箱执行器。Home 默认开启,桌面需显式开启;仍需准入、授权和同意,不是与系统 Agent 的通用对话。 | -| M6:渲染与评审(`card-studio`) | §7 | App Hub 的 `card-studio` crate 已合入([App-Hub#19](https://github.com/OctoSense-org/OctoSense-App-Hub/pull/19));对应 skill 规划中([#65](https://github.com/OctoSense-org/OctoSense/issues/65))。 | -| M7:应用对话、问题和记忆 | §9、§10 | 已实现人类/系统通道、Ask-app 面板、`sys.chat`、宿主路由的问题/审批及 peer/context 记忆。见[导读](architecture-walkthrough.zh-CN.md)。 | -| M8:外层循环(叠加在 `AGENT.md` 上的 overlay) | §11 | 规划中:[#67](https://github.com/OctoSense-org/OctoSense/issues/67)。 | - -跟踪 issue 是 [#68](https://github.com/OctoSense-org/OctoSense/issues/68)。文件格式见 App Hub 的 [PUBLISHING § The app's agent and tools](https://github.com/OctoSense-org/OctoSense-App-Hub/blob/main/docs/PUBLISHING.md#the-apps-agent-and-tools)。 +手机上的 glance 列表只显示摘要,不运行生成的界面。打开卡片会显示它的工作区,再次打开时状态依旧:手机上全屏,桌面端居中。发布者有 Agent 时,工作区有 Card / Chat 标签页,即使卡片没有声明 `sys.chat`;邮件回复则是 Email / Chat,共用一份保存的草稿。工作区见 README 的[卡片与提问](../README.zh-CN.md#卡片与提问);邮件的草稿、审阅和测试见[可组合的邮件卡片](mail-composable-cards.zh-CN.md)。 ## 本地运行与测试 -以下构建/启动配方 **unverified(未验证)**;依赖版本取自根 Cargo.toml。 +除 `--plan` 外,下面的命令都**未验证**。 ### 桌面端:使用临时的内核与配置 -1. 在 OctoSense 仓库根目录构建桌面内核。现有脚本读取 `Cargo.lock` 锁定的 octos 版本,并使用独立目录 `target/octos-kernel/`,不会切换其他检出的分支。构建和后续启动步骤均**未验证(unverified)**;本次只检查了 `--plan` 输出。 +1. 在仓库根目录构建桌面内核。这个脚本从 `Cargo.lock` 读取 octos 版本,在它自己的 `target/octos-kernel/` 中构建,不会动到其他检出;`--plan` 只打印步骤,不执行。 ```sh python3 tools/kernel-artifact.py --host --plan @@ -271,44 +210,40 @@ manifest 的 `agent` 和已准入 `tools.json` 已参与 Shell 的 Agent 发现 cargo run --release -p octosense ``` - 日志中会出现 `octos: kernel service ready (starts on first use), core dir …`。没有 `OCTOS_APP_CORE_BIN` 时使用 Shell 旁随附的 `octos-kernel`(`python3 tools/kernel-artifact.py --host --stage target/release`),两者都没有时日志会说明没有内核,AI providers 仍会保存提供方。 + 日志中会出现 `octos: kernel service ready (starts on first use), core dir …`。没有 `OCTOS_APP_CORE_BIN` 时,桌面端使用 Shell 旁随附的 `octos-kernel`(用 `python3 tools/kernel-artifact.py --host --stage target/release` 放到那里);两者都没有时,日志会说明没有内核,AI providers 仍会保存提供方。 -3. 打开 **Start → Settings → AI providers**,添加一个模型(family、模型、路由、密钥、**Test connection**、保存)。配置文件是 `$T/octos-home/.octos/profiles/_main.json`;使用文件密钥库时密钥就在其中,用完后请删除 `$T`。 -4. 通过某个使用者来使用助手:Rinx(默认链接并在进程内运行;从启动器打开它;登录 Matrix,在首次使用面板上允许它的 Agent,然后使用它的助手),或 AppCard(`--features app-appcard`)。隔离运行的应用通过 `octos` 宿主服务访问它([见上文](#隔离运行的脚本应用系统应用和商店应用));保持 `OCTOSENSE_CONTAINED_APPS` 未设置,并在 Shell 询问时同意。F8 也可打开系统聊天;`agents.ask` 用于同意/准备,`peer_send_input` 才委派任务。 +3. 打开 AI providers(Start → Settings → AI providers),添加一个模型及其密钥。配置文件是 `$T/octos-home/.octos/profiles/_main.json`;使用文件密钥库时密钥就在其中,用完后请删除 `$T`。 +4. 与助手对话:F8 打开系统对话;在首次使用面板上允许某个应用的 Agent 之后,就能在该应用的 “Ask <app>” 面板中与它对话。你自己的脚本应用通过 [`octos` 服务](#脚本应用与-octos-服务)访问它的 Agent,此时请保持 `OCTOSENSE_CONTAINED_APPS` 未设置。AppCard(`--features app-appcard`)是另一个使用方。 -**隐藏窗口。** 加上 `MAKEPAD_HIDE_WINDOWS=1 MAKEPAD_REMOTE=`,即可通过远程控制桥操作 Shell 而不占用屏幕([桌面端 README § Remote-control bridge](../desktop/README.md#remote-control-bridge))。`desktop/scripts/ai_providers_remote.sh` 以这种方式端到端运行 AI providers,使用假密钥并禁止出站 HTTPS;`desktop/scripts/glance_remote.sh` 对 glance 面板做同样的事。 +**隐藏窗口。** 加上 `MAKEPAD_HIDE_WINDOWS=1 MAKEPAD_REMOTE=`,即可通过远程控制桥操作 Shell 而不占用屏幕([桌面端 README § 远程控制桥](../desktop/README.zh-CN.md#远程控制桥))。`desktop/scripts/ai_providers_remote.sh` 以这种方式端到端运行 AI providers,使用假密钥并禁止出站 HTTPS;`desktop/scripts/glance_remote.sh` 以同样的隐藏方式操作 glance 面板。 **测试**(在仓库根目录): ```sh cargo test --locked -p octosense-kernel # 使用替身内核 cargo test --locked -p octosense-app-peers --features octos-core,ws # broker,脚本化内核 -cargo test -p octosense-ai-host --features octos-core,llm +cargo test --locked -p octosense-ai-host --features octos-core,llm cargo test --locked -p octosense-shell --lib approvals # 路由、规则、审批面板、同意、联系人 # 真实内核(按第 1 步构建): -OCTOS_CORE_TEST_KERNEL=/path/to/octos cargo test -p octosense-kernel --test real_kernel -- --nocapture -OCTOS_APP_PEERS_TEST_KERNEL=/path/to/octos cargo test -p octosense-app-peers --features octos-core --test real_kernel -- --nocapture +OCTOS_CORE_TEST_KERNEL=/path/to/octos cargo test --locked -p octosense-kernel --test real_kernel -- --test-threads=1 --nocapture +OCTOS_APP_PEERS_TEST_KERNEL=/path/to/octos cargo test --locked -p octosense-app-peers --features octos-core --test real_kernel ``` ### 手机 -- **Android(Home):** Home APK 把内核打包为 `liboctos.so`;配置好参数的 `rom/scripts/build-home.py` 流水线构建 APK 对(见 [phone/README.md](../phone/README.md)),其中使用 [`tools/kernel-artifact.py`](../tools/kernel-artifact.py) 按锁定的 octos 版本构建内核。内核在首次使用时启动。 -- 在 **OctoSense Settings → Accounts → AI providers** 中配置提供方,或者从桌面端迁移:在桌面端点 **Show QR for phone**,然后在手机上通过相机、图片或粘贴代码导入,并输入 PIN。 -- **OpenHarmony** 在进程内运行内核。**iOS** 没有内核:提供方会被保存,但没有应用能获得助手。 +- **Android(Home)** 把内核打包为 `liboctos.so`。`rom/scripts/build-home.py` 构建 APK 对(见 [phone/README.zh-CN.md](../phone/README.zh-CN.md)),其中使用 [`tools/kernel-artifact.py`](../tools/kernel-artifact.py) 按锁定的 octos 版本构建内核。内核在首次使用时启动。 +- 在 OctoSense Settings → Accounts → AI providers 中配置提供方,或者导入桌面端的代码(Show QR for phone),可以用相机、图片或粘贴文本,并输入它的 PIN。 +- OpenHarmony 和 iOS:见[内核服务](#内核服务)。 ## 源码位置 | 内容 | 位置 | | --- | --- | -| 内核服务 | [`crates/kernel`](../crates/kernel/README.md)(`src/launch.rs`、`src/lib.rs`) | -| Shell 入口、策略、offer | [`crates/ai-host/src/lib.rs`](../crates/ai-host/src/lib.rs) | -| 应用 peer:约定、broker、Shell 一侧 | [`crates/app-peers/src`](../crates/app-peers/src)(`contract.rs`、`broker.rs`、`hosted.rs`) | -| `llm` 服务、密钥库、面板 | [`apps/ai-providers/host-service/src`](../apps/ai-providers/host-service/src) | -| 隔离应用的 `octos` 服务 | [`crates/ai-host/src/contained.rs`](../crates/ai-host/src/contained.rs) | -| `model` 服务 | [`apps/ai-providers/host-service/src/complete`](../apps/ai-providers/host-service/src/complete) | -| Shell 注册的宿主服务 | `crates/shell/src/apps.rs`(`register_host_services`)、[`crates/shell/src/glance.rs`](../crates/shell/src/glance.rs) | -| 审批、同意、联系人、设置页面;开发者模式 | [`crates/shell/src/approvals/`](../crates/shell/src/approvals/mod.rs)、[`crates/shell/src/dev_mode.rs`](../crates/shell/src/dev_mode.rs) | -| 系统 Agent 的工具集 | [`crates/kernel/src/system_tools.rs`](../crates/kernel/src/system_tools.rs) | -| 其他(原生应用清单、托管、存储、协议) | [架构 § 源码位置](architecture.zh-CN.md#源码位置) | -| 权限、`tools.json`、agent 字段 | OctoSense-App-Hub [`crates/app-policy/src`](https://github.com/OctoSense-org/OctoSense-App-Hub/tree/main/crates/app-policy/src)(`manifest.rs`、`services.rs`、`agent.rs`) | -| Rinx 的助手和迷你应用适配器 | hagency-org/Rinx `src/assistant/`、`src/host/octos.rs` | +| 入口、宿主策略、`offer` | [`crates/ai-host/src/lib.rs`](../crates/ai-host/src/lib.rs) | +| 内核服务 | [`crates/kernel/src`](../crates/kernel/src)(`launch.rs`、`router.rs`、`system_tools.rs`) | +| 应用 peer:约定、代理、Shell 一侧 | [`crates/app-peers/src`](../crates/app-peers/src)(`contract.rs`、`broker.rs`、`hosted.rs`) | +| `octos` 服务 | [`crates/ai-host/src/contained.rs`](../crates/ai-host/src/contained.rs) | +| `llm` 与 `model` 服务、密钥库 | [`apps/ai-providers/host-service/src`](../apps/ai-providers/host-service/src)(`lib.rs`、`vault.rs`、`complete/`) | +| 哪些应用有 Agent,以及何时准备 | [`crates/shell/src/apps.rs`](../crates/shell/src/apps.rs)(`agent_apps`)、[`crates/shell/src/agents.rs`](../crates/shell/src/agents.rs) | +| 邮件草稿与发送审阅 | [`apps/mail/host-service/src/drafts.rs`](../apps/mail/host-service/src/drafts.rs)、[`crates/shell/src/mail_review.rs`](../crates/shell/src/mail_review.rs) | +| 其他 | [architecture.zh-CN.md § 源码位置](architecture.zh-CN.md#源码位置) | diff --git a/docs/architecture-walkthrough.md b/docs/architecture-walkthrough.md index 33bd3786..343f2155 100644 --- a/docs/architecture-walkthrough.md +++ b/docs/architecture-walkthrough.md @@ -2,301 +2,242 @@ English | [简体中文](architecture-walkthrough.zh-CN.md) -This walkthrough follows a message from an app window through the shell, agent kernel and tool executor. Start here if you know Rust but are new to OctoSense or agents. The [architecture reference](architecture.md) describes the design; this guide follows implementation calls and ownership. Read dependency versions from [Cargo.toml](../Cargo.toml) and [native-runtime.lock.json](../native-runtime.lock.json). ADRs record decisions; remaining implementation work is listed in §11. +This is the reading path through OctoSense's agent code. It follows one question from the window where the person types it, through the shell and the octos kernel, to the Rust that answers it. Each step names the file and symbol to open, what to look for, and why it matters. The concepts are in the README's [key concepts](../README.md#key-concepts); the full reference is [architecture.md](architecture.md). -We will use one question throughout: **“What is on my calendar today?”** On desktop, a person can ask the system assistant to delegate it to Calendar, or ask Calendar directly through “Ask Calendar”. Both routes can reach `calendar.events`, but their answers return to different conversations. This is an illustrative tool path: it requires a configured kernel/provider and consent, and the model chooses whether to call the tool. +The question is **"What is on my calendar today?"**, and Calendar is a system script app. The person can ask the system agent, which delegates it to Calendar's agent in the **system agent's lane**, or ask Calendar's agent directly, in its "Ask Calendar" panel or a Calendar card's Chat tab, in the **person's lane**. Both routes can end in the same tool call, `calendar.events`, and each answer returns to the conversation that asked. Either needs a kernel, a provider and the person's consent. -## 1. Give each name one meaning +## 1. Start at the executable -| Name | Meaning in this walkthrough | -| --- | --- | -| OctoSense | The desktop/Home shell, apps and their host services. The ROM packages Home with Android platform components. | -| Makepad | Rust UI/event/rendering framework. A widget tree is updated by its event loop. | -| Splash / Makepad Script | The script VM and UI language used by contained `main.splash` apps. This is not the separate Octoscript L0 parser. | -| Octoscript and Octoscript-Makepad | The L0 card language/checker/lowering and its Makepad integration, alongside runtime support for apps. L0 `.card` content and a contained `main.splash` program follow different loading paths. | -| octos | The agent kernel: model calls, turns, tools, transcripts, memory and peer coordination. It is not an operating-system kernel. | -| Agent | A configured model-driven worker: it reads messages, calls available tools, consumes their results and produces an answer. Model output does not itself perform an app operation. | -| Session / turn | A session identifies a conversation and its state. A turn is one execution responding to input; it can include several model requests and tool calls. | -| Peer / request context | A peer is a durable cooperating agent identity. A context is another session belonging to that peer, with its own transcript, workspace and a separate memory area under the peer’s namespace. | -| Tool / host service | A tool is an operation offered to the model. A host service is Rust code called by an app or tool executor. Exposing one API does not automatically expose the other. | -| `AGENTS.md` / `AGENT.md` | Repository contributor instructions / an app-bundle agent-instructions artifact. The bundle artifact is admitted by App Hub and supplied as host-guidance text on each turn; this is not kernel-native skill installation. | +[desktop/src/main.rs](../desktop/src/main.rs) only calls `octosense_shell::octosense_main!()`; [phone/src/main.rs](../phone/src/main.rs) makes the same call around an `App` that adds the Settings app. In [crates/shell/src/lib.rs](../crates/shell/src/lib.rs), `App::handle_startup` sets up the agent machinery in a fixed order: -The companion code tours cover [Design Flow](https://github.com/OctoSense-org/OctoScript-App-Design-Flow/blob/218b25d2460d64f843932f67d419467618464fb9/docs/CODE-WALKTHROUGH.md), [App Hub](https://github.com/OctoSense-org/OctoSense-App-Hub/blob/19bb52d402e80e89e085dea989615e3ec612d359/docs/CODE-WALKTHROUGH.md), [Octoscript-Makepad](https://github.com/OctoSense-org/OctoScript-Makepad/blob/fb29b6b1cb6e16f38d99a8aef60431a9565dfd59/docs/architecture-walkthrough.md) and [octos](https://github.com/octos-org/octos/blob/82900bf149d3a53016c1c1492ffc075d2d4fb0ed/docs/octosense-integration-walkthrough.md). These links select published documentation revisions; runtime versions remain controlled by each consumer's dependency pins. +| Call | What it sets up | +| --- | --- | +| `app_storage::init` | Every app's folders and the secrets root ([§8](#8-where-the-data-lives)) | +| `ai_host::start` | The kernel's configuration and the AI services ([§3](#3-find-who-owns-the-kernel)) | +| `dev_mode::init`, `approvals::init` | Developer mode, then the approval router | +| `host_tools::init` | The shell as every broker's tool host, with the relay ([§7](#7-trace-a-tool-to-rust-code)) | +| `system_chat::init` | The system agent's grants, before the kernel first starts | +| `agents::start` | A thread that prepares the peer of every script app the person allowed ([§5](#5-prepare-a-peer-and-give-it-two-lanes)), and Mail's event worker | -## 2. Start at the executable, then follow the app host +The order is the point: the router and the relay exist before any agent can call a tool. If the person allowed Calendar's agent in an earlier run, `agents::start` prepares its peer at once, and that first connection starts the kernel. -Open [desktop/src/main.rs](../desktop/src/main.rs), [phone/src/main.rs](../phone/src/main.rs), then [crates/shell/src/lib.rs](../crates/shell/src/lib.rs). Desktop and Home are two packages linking the shared shell. The phone also supplies Settings and platform integration. The ROM does not supply a second Rust agent architecture: it packages Home and privileged Android services. +## 2. See how an app is hosted -The launch choices come from [native-apps.json](../native-apps.json), [AppRegistry in apps.rs](../crates/shell/src/apps.rs) and each packaging's `system-apps.json`: +Where an app runs decides how it reaches its agent and where its tools execute. [native-apps.json](../native-apps.json) declares each native app's hosting, and `AppRegistry::hosting` in [apps.rs](../crates/shell/src/apps.rs) resolves it for each launch. -| What you launch | Follow the code | What is actually running | +| Kind | Open | Look for | | --- | --- | --- | -| Native Rust module, such as Reference or Rinx | [module_host.rs](../crates/shell/src/module_host.rs): module creation, scoped handles, assistant/storage offers | Rust code and UI in the shell process; each instance gets a script isolate. The isolate does not isolate arbitrary Rust memory. | -| Native process app, currently desktop Terminal where supported | [clients.rs](../crates/shell/src/clients.rs), [hub.rs](../crates/shell/src/hub.rs), [process-apps](../crates/process-apps/src/lib.rs) | A child executable exchanges frames, input and AI-bus messages over the authenticated Makepad hub socket. An `octos_peer` envelope on that socket also supports its assistant link; see §5. Shipped process apps currently request no `agent.octos` services. | -| Contained script app | App Hub's `CARD_MODULE`, reached through `apps.rs` | The Card runner admits the bundle and makes a restricted, nested Splash VM; capabilities limit access to host services and platform APIs. No Rust executable is compiled per bundle. | -| Glance card | [glance.rs](../crates/shell/src/glance.rs), [glance_chat.rs](../crates/shell/src/glance_chat.rs) | An L0/L1 `source` card is checked and lowered with host data; a Splash `script` card runs interactive handlers under its publisher's policy. `notify` adds a toast/shade entry. UI dismissal calls `glance::dismiss`; apps use `glance.withdraw`. Chat reaches the publisher's peer. | - -Read the product tutorials for command details: [desktop](../desktop/README.md), [Home](../phone/README.md), [ROM](../rom/README.md), and [system apps](../apps/README.md). A minimal developer route is below. **Launch/build recipes below are unverified; platform prerequisites are in the product READMEs.** Run from this repository root: - -```sh -python3 tools/setup.py --hub /path/to/existing-clones -python3 tools/setup.py --check --cargo -cargo run --release -p octosense -# An opt-in, in-process Rust example: -MAKEPAD_WM_TEST_APP=reference cargo run --release -p octosense --features app-reference -- --module reference -# A shell with the kernel integration; point at a binary built from Cargo.toml's octos rev: -OCTOS_APP_CORE_BIN=/path/to/pinned/octos \ - cargo run --release -p octosense --features octos-core -``` +| Native module (Rinx, Notes, Calculator, …) | [module_host.rs](../crates/shell/src/module_host.rs) | One Splash isolate per instance. Every call into a module runs under `catch_unwind` (`contain`), so a panic closes the app, not the shell. | +| Process app (the Terminal and Task, in a checkout build) | [clients.rs](../crates/shell/src/clients.rs), [hub.rs](../crates/shell/src/hub.rs) | The hub admits a child's socket only with the secret its launch read on stdin; `sandbox_policy` builds the OS sandbox. | +| Script app (Calendar, Mail, every store app) | `system_card_apps` in `apps.rs`, then App Hub's `CARD_MODULE` | The `card` module, the Card runner, hosts every system and installed app, one isolate per instance. | -The AI providers host-owned sheet must also configure a provider/model before a real conversation can run. A Rust build does not provision those credentials. `OCTOS_APP_CORE_BIN` names a file; this kernel launcher does not search `PATH`. For automated UI work use the existing hidden-window instructions in the product README. +`system_card_apps` makes Calendar's launcher row and, the first time, registers the shell's host services (`register_host_services`). Calendar's window lists no events, because App Hub has no `calendar` capability to grant it; only its agent's tools reach them. -For a script app, Design Flow's Python `tools/octo` CLI wraps App Hub's `card-host` and `hub` commands. Run a bundle with `tools/octo run /path/to/bundle` from the Design Flow checkout (unverified launch recipe). The standalone runner exercises contained UI and policy. Use a shell to test Mail, Calendar, provider and peer services; those are registered by the shell. A `hub check` pass validates the bundle's admission contract. +To run the desktop, follow its README's [Build and run](../desktop/README.md#build-and-run), which stages the pinned kernel with `python3 tools/kernel-artifact.py --host --stage target/release`. ## 3. Find who owns the kernel -Read [ai-host/src/lib.rs](../crates/ai-host/src/lib.rs), then [kernel/src/lib.rs](../crates/kernel/src/lib.rs), [launch.rs](../crates/kernel/src/launch.rs), [kernel.rs](../crates/kernel/src/kernel.rs) and [router.rs](../crates/kernel/src/router.rs). +One service owns the kernel, and every consumer connects to it. Read these in order: -1. The shell calls `ai_host::start`, registering host services and configuring the kernel source. `model.complete` is a one-shot provider request; it does not create a peer or run a tool loop. -2. The first authorized consumer calls `Core::connect`. It obtains a logical `Connection`; subsequent consumers share the same kernel generation. -3. `launch::resolve` selects a desktop child (`OCTOS_APP_CORE_BIN` or an explicit program), Android's executable packaged as `liboctos.so`, or an embedded OpenHarmony service. iOS has no kernel here. -4. `kernel::supervise` owns the running process/task and frame pump. `Router` correlates consumer request IDs and session events over the one physical protocol connection. -5. The protocol is OUP, JSON-RPC messages and asynchronous notifications. Ordinary mode uses newline-delimited JSON over stdio. Enabling Talk to Octos selects a host-managed loopback WebSocket; it does not start an extra kernel for each client. -6. Provider changes restart the generation. Consumers must reconnect and rebind. A peer's memory outlives any `Connection`. - -The privileged Android service in the ROM is a different use of the word “agent”: it performs permitted platform operations through the Android bridge. It does not run the system LLM conversation. +1. **`ai_host::start`** ([ai-host/src/lib.rs](../crates/ai-host/src/lib.rs)) configures the kernel and starts nothing. It also registers the `octos` service, which gives script apps their peers, and `model`, which makes one-shot model calls with no peer or tools. +2. **`Core::connect`** ([kernel/src/lib.rs](../crates/kernel/src/lib.rs)): the first connection starts a kernel *generation*, and later ones attach to it. When the last one detaches the kernel stops, unless Talk to Octos is on. After a provider change, `restart` ends the generation and consumers reconnect. +3. **`launch::resolve`** ([launch.rs](../crates/kernel/src/launch.rs)): a `serve --stdio` child on the desktop (`OCTOS_APP_CORE_BIN`, or the packaged `octos-kernel` whose receipt names the pinned revision) and on Android (`liboctos.so`), `serve_io` inside the shell on OpenHarmony, nothing on iOS. +4. **`supervise`** ([kernel.rs](../crates/kernel/src/kernel.rs)): one task per generation owns the process and the frame pump. Before any consumer's frame, it sets the system agent's exact tool list (`session/tool_list/set`). +5. **`Router`** ([router.rs](../crates/kernel/src/router.rs)): consumers, such as the system chat and Calendar's broker, share one stream. Each request gets a kernel-unique id and its response goes to its sender only; each notification goes to the consumers that opened its session. ## 4. Follow a system-chat message -Open [system_chat/mod.rs](../crates/shell/src/system_chat/mod.rs), [session.rs](../crates/shell/src/system_chat/session.rs), [link.rs](../crates/shell/src/system_chat/link.rs), and [system_tools.rs](../crates/kernel/src/system_tools.rs). - -The assistant pane sends a `Command` to its worker. The `Driver` opens `_main:api:octosense#system`, reads history and starts a turn. Incoming events update a chat model, and `SignalToUI` wakes the Makepad event loop to draw a snapshot. +Open [system_chat/mod.rs](../crates/shell/src/system_chat/mod.rs), then [session.rs](../crates/shell/src/system_chat/session.rs) and [link.rs](../crates/shell/src/system_chat/link.rs): -The system agent is a session on the `_main` profile. The host narrows its kernel tools using `session/tool_list/set`; `SYSTEM_AGENT_TOOLS` includes `peer_list`, `peer_send_input`, `peer_gather` and `peer_respond`. Its normal command-execution route, if the person enables it, is the shell's `terminal.run` tool and approval UI. It is not octos's builtin `shell` tool. +- `spawn` starts the `system-chat` thread, which owns a `session::Driver`. The UI thread sends it `Command`s; the thread wakes the UI with `SignalToUI` when the chat changes. +- The `Driver` opens `SYSTEM_SESSION` (`_main:api:octosense#system`) and sends each message as `turn/start`. After every connect it registers the system agent's host tools on its link (`peer/tools/register` without a `peer`). +- `link::poll_for` polls the kernel with a waker that unparks the thread, so the chat needs no Tokio runtime. It stays connected only while its pane is open or a turn runs. -[agents.rs](../crates/shell/src/agents.rs) adds two useful host tools: `agents.list` discovers app agents and their availability; `agents.ask` opens first-use consent and waits for a ready peer slug—the identifier issued by the kernel for that peer. On success the system agent uses `peer_send_input` to submit the task; the ask call itself only prepares access. The person controls first-use consent. +None of the system agent's kernel tools (`SYSTEM_AGENT_TOOLS` in [kernel/src/system_tools.rs](../crates/kernel/src/system_tools.rs)) reads a calendar, so it must ask Calendar's agent. Two host tools from [agents.rs](../crates/shell/src/agents.rs), answered by the system chat itself (`agents::call`), get it there: -## 5. Prepare one app peer, then give it two lanes +- `agents.list` returns every app with an agent, whether the person allowed it, and its peer slug. +- `agents.ask` shows Calendar's first-use sheet if the person has not decided, and holds the call until they answer and the peer is ready; then it returns the slug. -Read [app-peers/src/contract.rs](../crates/app-peers/src/contract.rs) before the much larger [broker.rs](../crates/app-peers/src/broker.rs). The traits explain the boundary: +The system agent passes the slug, never the app id `os.calendar`, to `peer_send_input`. Its approvals go to the router, batched per turn; the pane never approves anything. -- `OctosAppService`: the scoped assistant handle an app receives. -- `OctosContext`: a conversation/request handle. `call(ContextOp, EventSink)` starts work. The event sink is the callback that receives progress, replies and completion as `ContextEvent`s. -- `ContextSpec`: host-authenticated account, instance and granted service names. -- `Broker`: the shell adapter that connects app requests to the kernel. It holds the connection, peer, contexts and in-flight requests behind `Arc`. +## 5. Prepare a peer and give it two lanes -A native module receives its handle through [hosted.rs](../crates/app-peers/src/hosted.rs) and [injection.rs](../crates/app-peers/src/injection.rs). A contained app reaches the same broker through [contained.rs](../crates/ai-host/src/contained.rs). A script agent is eligible when its manifest declares assistant services, an `agent` block, or the bundle carries `tools.json`. Eligibility still requires a kernel, consent and an active account where applicable. +Read [app-peers/src/contract.rs](../crates/app-peers/src/contract.rs), everything an app sees, before the much larger [broker.rs](../crates/app-peers/src/broker.rs): -Native process clients use [peer_link](../crates/shell/src/peer_link/mod.rs): `OctosPeer` messages share the authenticated hub socket in an `octos_peer` envelope. The shell derives the app/instance from the launched client, checks the exact `native-apps.json` `agent.octos` grants and consent, then forwards session/history/turn/interrupt operations to that app's broker. +- `OctosAppService`: one app instance's scoped service. `open_conversation` opens the person's lane, `open_context` a private context (Rinx's mini apps). +- `OctosContext::call(ContextOp, EventSink)`: one operation, whose events end with one `Complete`. +- `ContextSpec` (the account and granted services) and `TurnTrigger` (what started a turn), both stamped by the host ([§6](#6-where-the-person-talks)). -An in-process module can use the same client through a parked link claimed by `module_host`. The injected `OctosAppService` is another adapter to this broker. +**Calendar's peer.** A script app's peer belongs to the `octos` host service in [ai-host/src/contained.rs](../crates/ai-host/src/contained.rs). `contained::prepare` (from `agents::prepare`) and `contained::conversation` (from the "Ask Calendar" panel) share one broker, `card.os.calendar`, built by `hosted::launch` ([hosted.rs](../crates/app-peers/src/hosted.rs)). Calendar keeps no accounts, so the broker acts for `device`. `Broker::ensure_peer` then: -Process death cancels outstanding work and closes request contexts while keeping the durable peer; uncertain writes return `outcome_unknown`. +1. sends `peer/prepare` with the memory namespace `app/card.os.calendar/acct-`, `resume: true` and, for a new peer, the account folder as its workspace (`ToolHost::agent_workspace`); +2. refuses a kernel that did not honor that namespace; +3. keeps the peer's host token in a `PeerRecord` and opens the peer session, `…#peer-`: the system agent's lane; +4. registers the app's tools (`register_tools`, [§7](#7-trace-a-tool-to-rust-code)). A peer whose registration fails runs no turn. -`card.os.news` is the broker's app identity, not necessarily the kernel's generated peer slug. `peer/prepare` returns that slug, the peer session and a host credential; use the returned values. Peers are keyed by app and account. Accountless apps use `device`; Mail uses the host-reported signed-in account. `ensure_peer` resumes the recorded peer, verifies its namespace, and registers tools before a turn can run. +When a native app has two windows, only the oldest instance's broker drives the peer (`Broker::drives`; `take_over` when it closes). Calendar's one broker always drives: it registers the tools and takes the system agent's inputs. Its `on_peer_input` checks the account and consent (`ToolHost::admit_input`), answers `peer/input/reject` when it refuses, and otherwise starts the turn on the peer session, one at a time. -In the Calendar example, delegation runs on the system lane; “Ask Calendar” runs on a human lane. The **blackboard** below is the kernel’s shared record of peer work and results, which the system agent can read with `peer_gather`. +**The person's lane.** Each conversation handle gets a new request context, opened with `share_history` (`open_handle`), plus `read_parent` when the app declares `storage.agent_workspace: "account"` (`context_reads_account`). ```mermaid sequenceDiagram - participant H as Human - participant S as System agent session - participant K as octos - participant B as Shell broker - participant A as App peer session - participant C as App conversation context - S->>K: peer_send_input(peer, task) - K->>B: peer/input notification - B->>B: check account, consent, tool registration; queue input - B->>K: turn/start on peer session, with input id - K->>A: execute system lane turn - H->>B: Ask app: ContextOp::Turn - B->>K: peer/context/open with share_history; turn/start - K->>C: execute human lane turn - A-->>K: completed turn / peer result - K-->>S: blackboard result available to peer_gather - C-->>B: streamed events and completion - B-->>H: app conversation / in-card chat reply + actor P as Person + participant S as System agent + participant K as octos kernel + participant B as Calendar's broker + participant L1 as System agent's lane + participant L2 as Person's lane + S->>K: peer_send_input(slug, question) + K->>B: peer/input + B->>B: check account and consent, queue it + B->>K: turn/start on the peer session + K->>L1: run the system agent's turn + P->>B: Ask Calendar, TurnFrom with TurnTrigger::Person + B->>K: peer/context/open with share_history, turn/start + K->>L2: run the person's turn, in parallel + L1-->>K: turn completed + K-->>S: result on the blackboard, read with peer_gather + L2-->>B: streamed events, completion + B-->>P: reply in the Ask Calendar panel ``` -The app peer session is the **system lane**. `open_conversation` creates a request context with `share_history` for a **human lane**. They have separate transcripts and turn state; bounded recent text from the other lane is supplied read-only. Multiple human surfaces may create separate conversation contexts of the same peer. `open_context` is the non-sharing path for per-client work, such as Rinx mini apps. A request context is its own kernel session, but is not another app peer. +**Native apps** reach a broker of the same kind through the [peer link](../crates/shell/src/peer_link/mod.rs), over a process app's hub socket or the channel a module's `OctosPeer::open` parked (`claim_peer_links`). The shell takes the app's identity from the socket or instance, never from a frame. -The broker's `driver_of` and `take_over` handle multiple native instances sharing a peer: one broker drives the system-lane input queue. This prevents every open app window from independently running the same `peer/input`. Human-context work is separate and can run while that queue's current turn is active. +## 6. Where the person talks -## 6. Where a person talks, and where the answer goes +Every surface but the system chat opens the person's lane: -| Surface | Implementation and behavior | +| Where the person types | Code to open | | --- | --- | -| System assistant pane, F8 | `system_chat`: system session; routes app questions raised during system-delegated work here too. | -| “Ask ”, Shift+F8 | [app_chat/mod.rs](../crates/shell/src/app_chat/mod.rs): `agents::conversation`, subscription to both lanes and merged history. Send starts the person's context turn. | -| An app's own native chat | Injected `OctosAppService::open_conversation`; the app renders its events. | -| A script app's own chat | Exact granted `octos.session.open`, `octos.session.history`, `octos.turn.start`, `octos.turn.interrupt` calls through `host.request`. The shipped system-app agents are shell-driven without their scripts declaring these calls. | -| A published card's chat | `sys.chat` → [l0-chat](../crates/l0-chat/src/lib.rs) and [glance_chat.rs](../crates/shell/src/glance_chat.rs). Publisher checks bind it to the card's owning app. | - -Phone Glance renders compact summaries without executing generated UI. A tap uses that summary's screen rectangle to reveal `glance_sheet.rs` above the feed at full viewport size. `card_presentation.rs` owns the reversible Makepad frame animation; it does not spawn Tokio tasks, launch an app or change the app peer. Email and Chat share one row. The original email and directly editable draft are in `mail_clip.rs`; `card_chat.rs` virtualizes the transcript and pins the native composer above the keyboard. Both panes use the same authoritative draft. `chat_submit` checks the declared `sys.chat` source, publisher, account and thread, then issues a one-use editing capability for the saved draft revision. Mail consumes it through `mail.suggest_reply`; without it, the tool only proposes a suggestion. This never authorizes sending. Dismissal preserves the resident widgets/session and revokes pending review authority; retained clean workspaces are bounded, and invalidated accounts/publications are retired. Final sending still requires physical approval of the exact host review. +| System chat | `system_chat/session.rs`: the system session ([§4](#4-follow-a-system-chat-message)) | +| "Ask <app>" panel | [app_chat/mod.rs](../crates/shell/src/app_chat/mod.rs): `agents::conversation`, then `ContextOp::TurnFrom { trigger: TurnTrigger::Person }` | +| A native app's own chat, through the injected service | `OctosAppService::open_conversation`. Rinx uses only `open_context`, for its mini apps' private contexts. | +| Another native app's chat | Makepad's `OctosPeer` over the peer link: `octos.session.open` without a `client` ([peer_link/link.rs](../crates/shell/src/peer_link/link.rs)) | +| A script app's own chat | `host.request("octos.turn.start")`, served by `contained.rs` for the names its manifest declares | +| A card's Chat tab, or a card that declares `sys.chat` | [glance_chat.rs](../crates/shell/src/glance_chat.rs) and [l0-chat](../crates/l0-chat/src/lib.rs) | -The shared workspace also hosts Calendar, News, Photos, YouTube and other publishers. When an agent-enabled publisher omits `sys.chat`, `L0Session::for_card` adds a host-owned Card / Chat conversation without rewriting its L0 or Splash source. `ContextKind::Card` carries bounded publication data and current L0 state, scoped to the original publication account and a stable card thread. It grants no Mail edit token or additional tools. First use opens the existing permission sheet through Enable assistant; a publisher without an agent has no Chat tab. Existing explicit `sys.chat` declarations keep their threads. Thus shipped notice and Calendar cards can use native workspace chat as well as desktop “Ask ”. The explicit `OCTOSENSE_GLANCE_DEMO=mail` chat remains canned. Local L0 changes and interacted Splash isolates are exempt from clean-cache eviction; stale accounts cannot reopen their old publications. +**Card workspaces** ([in-card chat](../README.md#in-card-chat)). [glance_sheet.rs](../crates/shell/src/glance_sheet.rs) shows an opened card full screen on the phone, centred on the desktop. If the publisher has an agent and the card declares no chat, `L0Session::for_card` ([glance_card.rs](../crates/shell/src/glance_card.rs)) adds a host-owned `WorkspaceChat` behind Card / Chat tabs. `chat_submit` sends each turn through `glance_chat::perform_bound` to the account that published the card (`agents::conversation_for_account`), with the card's data and local state as context (`ContextKind::Card`), never as tools. Mail's reply cards show Email / Chat over one saved draft; a Chat turn carries a one-use token (`drafts::issue_chat_edit`) that lets `mail.suggest_reply` save the edit ([Composed Mail cards](mail-composable-cards.md)). -A bound Mail reply card adds a durable host path: `mail.propose_reply` creates -an account/email-bound draft, and `mail.publish_card` carries its `draft_id`. -`mail_card.rs` answers `sys.mail_draft` and checks Field-origin edits against the -displayed revision. Its `sys.mail_review` write requests the host's -`mail_review.rs` UI, never approval. `drafts.rs` owns the immutable attempt and -SMTP receipt; the agent cannot send directly. Chat includes the bound email, -acknowledged draft and bounded history in the existing Mail peer. Only verified -Android physical touch can approve in this iteration; desktop/accessibility -approval and complete paired-model phone acceptance remain pending. Follow the -[implementation and gate table](mail-composable-cards.md) before treating the -new flow as validated. +Not yet: no shipped app opens the person's lane from its own UI; the shell's panel and cards are the way in. +The trigger decides how far approvals trust a turn. Only the shell's own surfaces, the "Ask <app>" panel and the system chat, stamp `TurnTrigger::Person`; an in-process module could through the injected service, but none does. An app's `"trigger": "person"` becomes `AppSaysPerson` (`TurnTrigger::from_args`), which the relay hands the router as an app run (`trigger_of` in `host_tools/relay.rs`); a card's chat gets the same stamp. Rinx's mini apps send a bare `ContextOp::Turn`, which is `Unknown`; standing rules skip it. Mail's new-mail events ([agent_events.rs](../crates/shell/src/agent_events.rs)) run in this lane as `TurnTrigger::Incoming`; no other app has events yet. -Phone touch navigation does not yet expose a control to open the Ask-app panel. App-owned chat and published-card chat remain separate surfaces. - -Stopping work depends on the entry point: - -| Action | What it interrupts | -| --- | --- | -| Ask-app panel’s Stop | The human lane | -| “Stop the system agent’s task” | The system lane | -| Lower-level `ContextOp::Interrupt` | Can interrupt both lanes | +For "Ask Calendar", the reply streams back through the context's `EventSink`, and the panel's follower (`OctosContext::subscribe`) hears both lanes. Not yet: a script app gets no pushed events; its `octos.turn.start` returns the finished reply. -Hiding the Ask panel keeps its context and subscription. Changing app or revoking access closes them. +Stop works per lane: the panel's Stop ends only the person's turn (`app_chat::stop`), and the system agent's turn has its own control (`stop_system_agent`). `ContextOp::Interrupt` on a conversation ends both, since the person owns the device. -The shell stamps a composer action as `TurnTrigger::Person`. A script can supply `trigger`/`from` with `octos.turn.start`, but its `trigger: "person"` becomes `AppSaysPerson`: a transcript label is not proof of a trusted human gesture and cannot unlock human-initiated approval rules. +## 7. Trace a tool to Rust code -An app agent's structured tool result returns to that app turn, not directly to whichever UI happens to be focused. Final human-context events go back through that context's event sink; a system-delegated turn makes its result available on the peer blackboard for the system agent to gather and summarize. +Follow `calendar.events` from its declaration to the file it reads: -## 7. Trace a tool to actual Rust code +1. **Declared** in [apps/calendar/bundle/tools.json](../apps/calendar/bundle/tools.json): its schemas, `risk: "read"`, `implemented_by: "host-service"`, and not `shareable`. +2. **Loaded** by `from_bundle` in [host_tools/script_apps.rs](../crates/shell/src/host_tools/script_apps.rs), through App Hub's digest-checking loader. `install` adds the tools to the relay's catalog, with a `HostServiceExecutor` for `os.calendar`. +3. **Registered** by `register_tools` in `broker.rs`, with what `ShellToolHost::declarations` ([host_tools/mod.rs](../crates/shell/src/host_tools/mod.rs)) returns. +4. **Called.** octos sends `peer/tool/call` on the registering link. The broker stamps the account, context and caller into a `HostToolCall` ([app-peers/src/host_tools.rs](../crates/app-peers/src/host_tools.rs)), which `ShellToolHost::tool_call` queues for the UI thread. +5. **Checked** by `Relay::handle` ([relay.rs](../crates/shell/src/host_tools/relay.rs)): the grant, consent, a signed-out account, the arguments' size and `input_schema`, then the caller's budget (by default 32 calls a turn and 1000 a day). +6. **Run.** `HostServiceExecutor::execute` dispatches a `ServiceCall` to App Hub's service registry as `os.calendar`, with no sheet. `CalendarService::call` ([apps/calendar/host-service/src/lib.rs](../apps/calendar/host-service/src/lib.rs)) loads `/.host/calendar/events.json` and filters it by `from`, `to` and `limit`. +7. **Answered.** `script_apps::poll` takes the reply from App Hub's queue, `checked_reply` (in `relay.rs`) checks it against `output_schema` and a size cap, and the `ToolReply` sends `peer/tool/result` once. -Read [host_tools/script_apps.rs](../crates/shell/src/host_tools/script_apps.rs), [relay.rs](../crates/shell/src/host_tools/relay.rs), and [app-peers/host_tools.rs](../crates/app-peers/src/host_tools.rs). +`calendar.events` only reads, so nobody is asked. `calendar.remove_event` (`destructive`, `confirm: host`) is gated in octos first: the kernel raises a `host_tool` approval, which the broker hands to `ToolHost::host_tool_approval`, and only an approved call arrives. -For a concrete example, follow Calendar's `calendar.events`: +The relay routes every call by the tool's owner: -1. [tools.json](../apps/calendar/bundle/tools.json) declares its schema, risk, sharing and implementation. App Hub validates/digests the bundle. -2. `script_apps::from_bundle` loads admitted declarations; `install` adds them to the relay catalog and installs a `HostServiceExecutor`. -3. The peer's driving broker registers the exact tool roster with `peer/tools/register`. A model can now request that declared tool. -4. octos emits `peer/tool/call`. The broker stamps the actual caller/account/context and passes it to the shell's `ToolHost`. -5. `Relay::handle` checks authorization, consent, account state, input schema, size and call budgets. Approval-gated calls enter the router described below. -6. The executor invokes the Calendar host service as the **owning app**, which reads [Calendar's store](../apps/calendar/host-service/src/lib.rs). The reply queue returns the outcome; output schema/size are checked and `ToolReply` sends at most one `peer/tool/result`. -7. The model consumes that JSON result and continues its turn. It may answer in text or call another granted tool. - -The declaration is not the implementation. `implemented_by: "host-service"` works only where the service exists and the owner may call it. `implemented_by: "app"` is admitted metadata, but the Card runner's script executor is not implemented; it returns unavailable. Neither a copied `tools.json` nor a successful gate check creates missing service code. - -The executor depends on the tool's owner. [host_tools/mod.rs](../crates/shell/src/host_tools/mod.rs) collects calls from broker threads into an inbox; the UI event loop pumps that inbox and queues replies. - -| Tool route | Executor boundary | +| Owner | Executor | | --- | --- | -| Contained app, `implemented_by: "host-service"` | `HostServiceExecutor` invokes the admitted owner's Rust service. | -| Native module | Its `OctosAppService::set_tool_executor` callback handles the call. | -| Native process | `peer_link` sends a tool request to the client over its authenticated hub socket; registration and grants are still required. | -| System `terminal.run` | The shell types an approved command into the visible Terminal through its AI bus; enabled by the Command execution setting. | -| `files.list/read/search` | Shell executor over the caller's permitted account workspace, on Unix. | -| `dev.run` | Shell executor available to peers covered by developer mode. | -| Toolbox tools | Registered toolbox executor/workflow, behind the `toolbox-peers` feature and the app's grants. | +| Script app, `implemented_by: "host-service"` | `HostServiceExecutor`: the app's host service (`calendar`, `mail`, `news`), or the shell's notice service for `.notify` | +| Script app, `implemented_by: "app"` | None yet: the call is refused `app_tool_unavailable` | +| Native app | Its open instance: `OctosPeer::serve_tools` on its peer link, else its AI bus service ("Open … first" when closed). An executor from `OctosAppService::set_tool_executor` comes first. | +| `terminal.run` (system agent only) | The visible Terminal, over the AI bus, after a sheet with the exact command | +| `files.list`, `files.read`, `files.search`, `dev.run` | The shell itself | +| The toolbox (feature `toolbox-peers`) | The toolbox's executor | ### Approval order -First-use agent consent, tool grants and per-call approval are separate checks. For an approval request, [approvals/router.rs](../crates/shell/src/approvals/router.rs) applies this order: +First-use consent, a tool grant and a per-call approval are separate checks. For an approval, `Router::request` in [approvals/router.rs](../crates/shell/src/approvals/router.rs) tries, in order: -1. An external client's turn stays with that client: the shell neither answers nor expires its prompts. -2. Developer mode approves requests for the apps it covers, including `auto_approvable: false` and `confirm: app`. -3. A `confirm: app` tool goes to its owning app's registered confirmation sheet, with the caller shown. Standing rules do not answer it. If no sheet registers before `app_wait_s`, the request is refused. -4. Requests marked `auto_approvable: false`, unknown outcomes, and calls classified as external connections require the person. -5. The person's standing rules may decide eligible requests. Incoming-content runs are skipped unless a rule explicitly includes them. -6. Otherwise, a shell confirmation sheet asks the person; calls from one system task may be batched. +1. the external client's own prompt, for that client's turns; +2. developer mode, for the apps it covers; +3. the owning app's sheet, for a `confirm: app` tool (refused after `app_wait_s`, 120 seconds, if the app registers none); +4. a live sheet, for calls that must always ask: `auto_approvable: false`, an unknown outcome, an external connection; +5. the person's standing rules; +6. otherwise, a shell sheet with the exact arguments. -Each decision is delivered once and recorded in an owner-only audit log; automatic decisions also produce a notice. A held host-connection request expires as denied after `OCTOSENSE_PROMPT_DEADLINE_SECS` (default ten minutes). The host-tool audit stores an argument digest rather than raw arguments. A model's text answer or `peer_respond` cannot approve a tool call. +Decisions are audited in `logs/approvals-audit.jsonl`, and the deadlines are `DEFAULT_PROMPT_DEADLINE` (10 minutes) and `EXPIRY_GRACE` (30 seconds) in `app-peers/src/host_tools.rs`. The system agent cannot approve: octos refuses its `peer_respond` for approvals. What each step means is in [architecture.md §5](architecture.md#5-approvals). -## 8. What “access the app's data” actually means +Sending mail never reaches this router. `mail.propose_send` only prepares the exact message; the host's review in the card (`mail_review.rs`) calls `drafts::approve_and_send` only after a physical touch (`trusted_user_gesture`, Android only for now), and developer mode cannot stand in for it. -There are several stores, with separate ownership: +## 8. Where the data lives -| Data | How an app agent reaches it | -| --- | --- | -| App account folder | [app_storage](../crates/shell/src/app_storage/mod.rs) and `ToolHost::agent_workspace` bind a new peer's cwd to the permitted folder. Kernel file tools still need grants. | -| Account text files from a request context | [files.rs](../crates/shell/src/host_tools/files.rs): shell-owned `files.list/read/search`, currently Unix only, only with consent and an available workspace. They are bounded, reject symlink traversal and hide sibling contexts. They are not SQLite query APIs. | -| Human conversation's parent account folder | `read_parent` is requested only where `storage.agent_workspace: "account"` allows it. Parent is read-only; context writes stay in its own folder. Plain client contexts do not get this automatically. | -| Host-service database or remote account | A specific declared tool, implemented by the owning host service. Workspace access does not mount every host database or grant remote credentials. | -| Agent transcript and memory | octos session/context and `app//acct-` namespaces. These are separate from the app's business data. | -| Secrets | Host-owned secret store/sheets; not the agent workspace or script state. | - -For the Calendar question, the executor reads `calendar/events.json` under Calendar’s host directory through `calendar.events`. The model receives the service’s result; it does not open the file itself. Other app tools have narrower scopes: +An app's data lives in several stores, each with one owner: -| App | Current data/tool boundary | -| --- | --- | -| News | `news.list`, `news.read` and `news.notify` | -| Mail | Account-scoped accounts/folders/sync/list/peek, notify/publish_card/skip_event, plus propose_reply/draft/suggest_reply/propose_send; no send, approve or credential tool | -| Photos, Maps, Camera, YouTube | Only their own `.notify`, publishing a shared notice card through [glance_notice.rs](../crates/shell/src/glance_notice.rs) | -| AI providers | No app agent | +| Data | Where | How an agent reaches it | +| --- | --- | --- | +| The account folder | `apps//accounts//`, or `accounts/device/` ([app_storage/mod.rs](../crates/shell/src/app_storage/mod.rs)) | It is the peer's workspace, fixed when the peer is created. The person's lane runs in its own `contexts//` and reads the folder through `read_parent` or the host's `files.*` tools ([files.rs](../crates/shell/src/host_tools/files.rs): Unix only; 128 KiB a read, 500 entries, 100 matches). | +| A host service's data | App Hub's host directory, `/.host/`: Calendar's events, Mail's messages and reply drafts (`drafts.rs`) | Only through that service's tools; no workspace includes it. | +| Transcripts and memory | octos, under the namespace `app//acct-` | The agent's own. `` is an FNV-1a hash (`account_tag`); the folder name is a different, SHA-256 hash (`account_hash`). | +| Secrets | App secrets: the keychain on macOS and iOS (indexed in `/secrets//`), elsewhere files there. Provider keys: the macOS keychain, elsewhere files under the kernel's core directory (owner-only, except on Windows) | Never. `app_storage::check` refuses a workspace that contains or links to them. | -The older AppCard personal-data importer is not automatically synchronized with current Mail’s store. +`agent_workspace_in` ([host_tools/mod.rs](../crates/shell/src/host_tools/mod.rs)) gives the folder to every native app with `octos.*` services, even one that declares `storage.agent_workspace: "none"`, and to every script app that does not. Calendar's agent gets `apps/os.calendar/accounts/device/`, but its events are not there: `calendar.events` reads them from `.host/calendar/events.json` and hands the model JSON. -Account directory names use a SHA-256-derived tag; peer memory names use the broker's FNV-derived tag. These are compatibility identifiers, not interchangeable paths. Sign-out suspends access and retains data; account removal/uninstall additionally requests `peer/purge`, retrying busy peers. A peer's saved workspace cannot silently change on resume. +Signing out suspends the peer, whose calls are then answered `signed_out`. Removing the account or uninstalling the app erases it with `peer/purge` ([purge.rs](../crates/app-peers/src/purge.rs)). ## 9. Cross-app work and asking for help -Three operations are easy to confuse: +Three operations are easy to confuse. -**Delegation:** the system agent uses `peer_send_input` to ask an existing app peer to do work, then gathers its answer. The shell remains responsible for driving that app turn. The peer is not a newly spawned process. +**Delegation.** The system agent asks an app's agent with `peer_send_input` ([§4](#4-follow-a-system-chat-message)), and the shell starts that turn on the existing peer. No process is spawned. -**Calling another app’s API as a tool:** this skips the owning app’s model and invokes its executor directly. For example, another app could call a shared Calendar tool only when all of these prerequisites hold: +**Calling another app's tool directly.** This skips the owning app's model, so the relay allows it only when all of these hold: -1. The tool has a declaration with `shareable: true` and an executable handler. -2. The caller has a grant. Script manifests request dotted names in `agent.tools`; native apps use their reviewed grants. `may_call` checks these rules, with explicit developer-mode exceptions. -3. For a script bundle, App Hub admission—the check that the host accepts the bundle—allows the name through `HostLimits.offered_tools`. -4. `Catalog::owner_of` can resolve the owner, so the relay can call that owner’s executor. +1. The owner declares the tool `shareable: true` and has an executor for it. +2. The caller is granted it: by a dotted name in a script app's `agent.tools`, or by `agent.grants` in a native app's `native-apps.json` entry. `Catalog::may_call` checks this. +3. For a script bundle, App Hub's admission offers the name (`HostLimits.offered_tools`). +4. `Catalog::owner_of` finds the owner from the namespace: the toolbox, the native app of that id, or else the system app `os.`. -The executor controls access to the receiving app’s data. Current owner resolution covers native namespaces, toolbox namespaces and `os.`; it does not discover arbitrary installed store apps. Default admission does not offer arbitrary names such as `mail.send`, and Mail does not currently implement that agent tool. A relay route alone cannot make it callable. +None of Calendar's tools is shareable, so other agents must ask Calendar's agent; News shares `news.list` and `news.read`. Not yet: `owner_of` never resolves to a store app, so store apps cannot share tools. -**A question or system facility:** an app agent can call a granted `ask_user_question`. [questions/mod.rs](../crates/shell/src/questions/mod.rs) routes it by the initiating turn: system chat for system-delegated work, app conversation for a human/app turn. The person answers on a shell surface. Apps receive system facilities as explicitly granted tools, such as [toolbox](../crates/toolbox/README.md) workflows. A general app-to-system-agent conversation RPC remains unimplemented. `peer_respond` handles peer coordination; approval messages have separate host-owned answer handles. +**Asking for help.** [questions/mod.rs](../crates/shell/src/questions/mod.rs) routes an agent's `ask_user_question` by the turn's origin: to the system chat for a `peer/input` turn, to the app's conversation otherwise. Only the person answers, on a shell surface. System facilities reach an app's agent only as granted tools, such as the [toolbox](../crates/toolbox/README.md)'s workflows. Not yet: an app cannot start a conversation with the system agent; `OctosAppService` has no call for it. ## 10. Map the architecture to Rust execution -A peer is persisted identity and state. A **Tokio task** is an async computation scheduled on a runtime’s worker threads; waiting for I/O lets that worker run another ready task. A turn uses several tasks, and the peer survives their completion. - -Follow the direct “Ask Calendar” question across those boundaries: - -1. The Makepad UI submits a context request to Calendar’s broker. -2. The broker’s runtime sends `turn/start` through a shared kernel `Connection`; the kernel-service supervisor carries it across the transport. -3. octos spawns a turn orchestration task. It waits at a start barrier until the active-turn registry accepts it, then starts agent processing. -4. If the model calls `calendar.events`, its tool future waits while the broker and shell relay deliver the request to Calendar’s host service. The returned data lets the model continue; reply events travel back to the human context and wake the UI. +A peer is stored state and a turn is a group of Tokio tasks in octos; threads belong to services, not agents ([why it stays light](../README.md#why-it-stays-light-on-memory-and-cpu)). -These are scheduling boundaries, not a new thread or process for every peer. Use this table to locate their owners: - -| Layer | Actual execution model | Source | +| Layer | How it runs | Where to look | | --- | --- | --- | -| Makepad shell | UI event loop; widget drawing, event dispatch and host-tool relay pump | `module_host.rs`, `host_tools/mod.rs` | -| System chat | An ordinary `std::thread`; commands and snapshots; `link::poll_for` uses a waker/unpark to poll kernel receive | `system_chat/mod.rs`, `link.rs` | -| Shell kernel service | Lazily built Tokio runtime: **2 worker threads**, **8 MiB stacks**. One generation supervisor task owns transport and process lifecycle, with auxiliary I/O tasks | `kernel/src/lib.rs::Inner::runtime`, `kernel.rs::supervise` | -| App broker | **Each `Broker::new` builds a runtime with 1 worker thread**. Link pump, request futures, retries and deadline tasks run there. Multiple brokers may share one durable peer | `app-peers/src/broker.rs` | -| Embedded OpenHarmony kernel | `serve_io` is spawned on the host runtime over `tokio::io::duplex`; no child executable | `kernel.rs::start` | -| octos OUP turn | A spawned orchestration task waits on a `oneshot` start barrier until active-turn admission succeeds; `run_standalone_turn` then spawns agent processing and auxiliary tasks | pinned octos `crates/octos-cli/src/api/ui_protocol_transport.rs` | -| octos transport output | WebSocket has an async writer task; embedded/stdio uses a bounded synchronous queue and an ordinary writer thread. Transport variants are not identical | same octos file | -| Host service / file executor | Depends on the service: UI pump, callback/reply queues, worker threads for blocking operations. Not universally a Tokio task per service or app | `host_tools/files.rs`, App Hub `services.rs`, app host services | +| Shell UI | The Makepad UI thread: drawing, events, and `host_tools::pump` with the relay | `lib.rs`, `host_tools/mod.rs` | +| System chat | One `std::thread`, polling the kernel with `link::poll_for` | `system_chat/mod.rs`, `link.rs` | +| Kernel service | One Tokio runtime, built on first use: 2 workers, 8 MiB stacks; one supervisor task per generation | `kernel/src/lib.rs` `Inner::runtime`, `kernel.rs` `supervise` | +| App broker | A runtime per `Broker::new`, with 1 worker: the link loop, requests, retries, deadlines | `app-peers/src/broker.rs` | +| Host services | Called on the caller's thread by App Hub's `services::dispatch`: for a tool call, the UI thread, where Calendar answers. Mail (`work` threads, `mail-fetch`) and News (`news-fetch`) run network work on their own threads. | `script_apps.rs`, `apps/*/host-service/` | +| octos, desktop and Android | Its own process, on Tokio's default runtime: one worker per CPU core (`ServeCommand::execute`) | octos `crates/octos-cli/src/commands/serve.rs` | +| octos, OpenHarmony | `serve_io` on the kernel service's runtime, over `tokio::io::duplex` | `kernel.rs` `start` | +| An octos turn | A spawned task behind a `oneshot` start barrier, then `run_standalone_turn` and its own tasks | octos `crates/octos-cli/src/api/ui_protocol_transport.rs` | ```mermaid flowchart LR - UI["Makepad UI thread"] --> CMD["System-chat std thread"] - UI --> B["Broker runtime(s): request and link tasks"] + UI["Makepad UI thread
relay, host services"] --> CMD["system-chat thread"] + UI --> B["Broker runtime, 1 worker
link loop, requests"] CMD --> C["Kernel Connection channels"] B --> C - C --> SUP["Kernel-service supervisor task"] - SUP <-->|"stdio or host WebSocket"| OUP["octos protocol dispatcher"] - OUP --> T1["System-lane turn task"] - OUP --> T2["Human-context turn task"] - T1 --> TOOL["tool future waits for host reply"] - TOOL --> B - B --> Q["Shell relay queue"] - Q --> UI + C --> SUP["Kernel service runtime, 2 workers
generation supervisor task"] + SUP <-->|"stdio, or the host WebSocket"| OUP["octos protocol dispatcher"] + OUP --> T1["System agent's lane
turn task"] + OUP --> T2["Person's lane
turn task"] + T1 --> TOOL["Tool future waits
for peer/tool/result"] + T2 --> TOOL + TOOL -->|"peer/tool/call"| B + B -->|"host_tools::submit"| UI ``` -Read `mpsc` as a many-sender mailbox, `oneshot` as one correlated answer, and `watch` as the latest lifecycle/readiness state. In the broker, request IDs map replies to `oneshot` senders; its link loop uses `tokio::select!` for outbound and inbound traffic. The kernel supervisor selects control messages, kernel output and process exit. `Arc` shares ownership, `Weak` avoids keeping a dropped broker/context alive, and generation/epoch checks reject replies from an obsolete account or connection. +For "Ask Calendar", octos's `handle_turn_start_with_accept` spawns the person's turn task, and its `calendar.events` future waits while the call crosses to the UI thread and back. + +Three channel kinds recur: `oneshot` for one answer (a request's reply, a turn's start), `mpsc` for a mailbox (the supervisor's `Ctl` messages) and `watch` for the latest state (a generation's readiness). The broker's link loop and the supervisor are both `tokio::select!` loops, and the account generation and `link_epoch` checks drop replies meant for an old account or connection. -The broker's synchronous `bind`/`host_request` wrappers wait for channel replies: do not call such blocking interfaces from a rendering callback. Async model I/O can overlap across sessions; synchronous filesystem work still needs the existing worker boundary. One peer per app and account is an identity rule. The number of tasks and worker threads follows the runtimes and active operations above. +`Broker::bind`, `Broker::host_request` and `OctosAppService::prepare` block their caller for up to a minute, so call them off the UI thread. -## 11. Tests and remaining implementation +## 11. Tests -[Broker tests](../crates/app-peers/tests/broker.rs) provide executable protocol examples: `a_persons_message_runs_while_the_system_agents_input_runs`, `a_lane_stop_leaves_the_other_lane_running`, `a_kernel_without_shared_history_is_refused_for_the_conversation`, and `removing_an_account_purges_its_recorded_peer_and_drops_the_record`. [Shell relay scenario tests](../crates/shell/src/host_tools/scenario_tests.rs) cover caller/tool/approval boundaries. Start with these when changing a conversation or executor path. +[crates/app-peers/tests/broker.rs](../crates/app-peers/tests/broker.rs) drives the broker against a scripted kernel; its test names state the protocol's rules. Start with `a_persons_message_runs_while_the_system_agents_input_runs`, `a_lane_stop_leaves_the_other_lane_running`, `a_kernel_without_shared_history_is_refused_for_the_conversation` and `removing_an_account_purges_its_recorded_peer_and_drops_the_record`. [host_tools/scenario_tests.rs](../crates/shell/src/host_tools/scenario_tests.rs) runs the same two-lane story with a test News app end to end against the pinned octos and a scripted model; its header says how to run it. Run the unit and scripted-connector tests from the repository root: @@ -305,6 +246,6 @@ cargo test --locked -p octosense-kernel -p octosense-app-peers \ --features octosense-app-peers/octos-core,octosense-app-peers/ws ``` -Optional real-kernel tests return early when their binary environment variables are absent; see the [app-peers test instructions](../crates/app-peers/README.md) before treating those as integration evidence. Visible UI, real-provider conversations and device behavior require separate runs. +The real-kernel tests (`crates/app-peers/tests/real_kernel.rs` and the scenario tests) print a note and pass when no kernel binary is set, so a green run is not integration evidence; the [app-peers README](../crates/app-peers/README.md#testing) shows how to give them one. Visible UI, real providers and devices need runs of their own. -Mail now has an opt-in incoming-event dispatcher. Admitted `AGENT.md` and skill text accompany each turn, with account-scoped preferences provisioned by the system agent; see the [Mail event walkthrough](mail-agent-events.md). General triggers/cron, kernel-native skill installation, script-implemented agent tool dispatch and a general app-to-system-agent conversation RPC remain unimplemented. +Walkthroughs of the other repositories, at fixed revisions: [Design Flow](https://github.com/OctoSense-org/OctoScript-App-Design-Flow/blob/218b25d2460d64f843932f67d419467618464fb9/docs/CODE-WALKTHROUGH.md), [App Hub](https://github.com/OctoSense-org/OctoSense-App-Hub/blob/0d5b47a2ae9eb98020feca26b7c895a3cf797dc1/docs/CODE-WALKTHROUGH.md), [Octoscript-Makepad](https://github.com/OctoSense-org/OctoScript-Makepad/blob/2cc5ef37d7d6a3d2992673389ce74488f7bb2d87/docs/architecture-walkthrough.md) and [octos](https://github.com/octos-org/octos/blob/056173e85b150e387805fc307fe231064ac1ed35/docs/octosense-integration-walkthrough.md). diff --git a/docs/architecture-walkthrough.zh-CN.md b/docs/architecture-walkthrough.zh-CN.md index bd80474b..c622778a 100644 --- a/docs/architecture-walkthrough.zh-CN.md +++ b/docs/architecture-walkthrough.zh-CN.md @@ -2,293 +2,250 @@ [English](architecture-walkthrough.md) | 简体中文 -本文沿着一条消息,阅读应用窗口、Shell、Agent 内核与工具执行器之间的调用。如果你了解 Rust,但刚接触 OctoSense 或 Agent,可以从这里开始。[架构参考](architecture.zh-CN.md)介绍整体设计,本文跟踪实现中的调用与所有权。依赖版本以 [Cargo.toml](../Cargo.toml) 和 [native-runtime.lock.json](../native-runtime.lock.json) 为准。ADR 记录设计决策,尚待实现的部分列在第 11 节。 +这是阅读 OctoSense Agent 代码的路线。它跟着一个问题,从用户输入它的窗口出发,经过 Shell 和 octos 内核,一直走到回答它的 Rust 代码。每一步都说明要打开哪个文件和符号、在那里看什么,以及为什么重要。概念见 README 的[关键概念](../README.zh-CN.md#关键概念);完整参考见 [architecture.zh-CN.md](architecture.zh-CN.md)。 -全文使用同一个问题:**“我今天有哪些日程?”** 桌面用户可以让系统助手委派给 Calendar,也可以直接通过 “Ask Calendar” 提问。两条路线都可能调用 `calendar.events`,但答案返回不同的对话。这是说明调用关系的例子:需要已配置的内核/提供方和用户同意,是否调用工具由模型决定。 +这个问题是 **“我今天有哪些日程?”**,日历是一个系统脚本应用。用户可以问系统 Agent,由它委派给日历的 Agent,在**系统 Agent 的通道**里处理;也可以直接问日历的 Agent,在它的 “Ask Calendar” 面板或日历卡片的 Chat 标签页里,走**用户的通道**。两条路线最后都可能调用同一个工具 `calendar.events`,答案各自回到提问的那段对话。两者都需要内核、提供方和用户的同意。 -## 1. 先分清这些名称 +## 1. 从可执行入口开始 -| 名称 | 本文中的含义 | -| --- | --- | -| OctoSense | 桌面/Home Shell、应用及其宿主服务。ROM 把 Home 与 Android 平台组件一起打包。 | -| Makepad | Rust UI、事件与渲染框架;事件循环更新控件树。 | -| Splash / Makepad Script | 隔离运行 `main.splash` 应用的脚本 VM 与 UI 语言;与 Octoscript L0 解析器分别实现。 | -| Octoscript / Octoscript-Makepad | L0 卡片语言、检查器、lowering 及 Makepad 集成,还提供应用运行时支持。`.card` 与 `main.splash` 有各自的加载路径。 | -| octos | Agent 内核,负责模型调用、回合、工具、对话记录、记忆与 peer 协作;这里的“内核”指 Agent 运行时。 | -| Agent | 读取消息、选择工具、消费结果并回答的模型工作单元。实际操作由工具执行器完成。 | -| Session / turn | Session 保存一次对话的身份与状态;turn 是响应一次输入的执行过程,可包含多次模型请求与工具调用。 | -| Peer / request context | Peer 是持久的协作 Agent 身份;context 是属于该 peer 的另一个 session,有独立记录、工作目录,以及 peer 记忆命名空间下的独立存储区域。 | -| Tool / host service | Tool 是模型可选择的操作;host service 是应用或执行器调用的 Rust 服务。分别检查它们的授权与入口。 | -| `AGENTS.md` / `AGENT.md` | 前者指导仓库贡献者;后者是应用包的 Agent 指令文件。App Hub 准入后者,Shell 每轮提供其宿主指令文本;这不是内核原生技能安装。 | +[desktop/src/main.rs](../desktop/src/main.rs) 只调用 `octosense_shell::octosense_main!()`;[phone/src/main.rs](../phone/src/main.rs) 做同样的调用,只是外面包了一个加入设置应用的 `App`。在 [crates/shell/src/lib.rs](../crates/shell/src/lib.rs) 中,`App::handle_startup` 按固定顺序搭起 Agent 相关的部件: -相关仓库的导读:[Design Flow](https://github.com/OctoSense-org/OctoScript-App-Design-Flow/blob/218b25d2460d64f843932f67d419467618464fb9/docs/CODE-WALKTHROUGH.md)、[App Hub](https://github.com/OctoSense-org/OctoSense-App-Hub/blob/19bb52d402e80e89e085dea989615e3ec612d359/docs/CODE-WALKTHROUGH.md)、[Octoscript-Makepad](https://github.com/OctoSense-org/OctoScript-Makepad/blob/fb29b6b1cb6e16f38d99a8aef60431a9565dfd59/docs/architecture-walkthrough.md)、[octos](https://github.com/octos-org/octos/blob/82900bf149d3a53016c1c1492ffc075d2d4fb0ed/docs/octosense-integration-walkthrough.md)。这些链接指向已发布的文档版本;运行时版本仍由使用方的依赖锁定文件控制。 +| 调用 | 建立什么 | +| --- | --- | +| `app_storage::init` | 每个应用的文件夹和机密根目录([第 8 节](#8-数据存放在哪里)) | +| `ai_host::start` | 内核的配置和 AI 服务([第 3 节](#3-找到内核的所有者)) | +| `dev_mode::init`、`approvals::init` | 先是开发者模式,再是审批路由 | +| `host_tools::init` | 让 Shell 成为每个代理的工具宿主,连同中转([第 7 节](#7-把工具追到-rust-代码)) | +| `system_chat::init` | 系统 Agent 的授权,在内核第一次启动之前交给它 | +| `agents::start` | 一个线程,为用户已允许的每个脚本应用准备 peer([第 5 节](#5-准备-peer给它两条通道)),以及邮件的事件工作线程 | -## 2. 从可执行入口读到应用宿主 +顺序正是关键:在任何 Agent 能调用工具之前,审批路由和中转就已经存在。如果用户在之前的运行中已经允许了日历的 Agent,`agents::start` 会立即准备它的 peer,而这第一个连接就会启动内核。 -先打开 [desktop/src/main.rs](../desktop/src/main.rs)、[phone/src/main.rs](../phone/src/main.rs),再读 [crates/shell/src/lib.rs](../crates/shell/src/lib.rs)。Desktop 和 Home 链接同一个 Shell;Home 还提供 Settings 与平台集成。ROM 打包 Home 和特权 Android 服务,沿用同一套 Rust Agent 架构。 +## 2. 看应用如何被托管 -应用启动方式来自 [native-apps.json](../native-apps.json)、[apps.rs 的 AppRegistry](../crates/shell/src/apps.rs) 与各产品的 `system-apps.json`: +应用在哪里运行,决定了它如何连到自己的 Agent,以及它的工具在哪里执行。[native-apps.json](../native-apps.json) 声明每个原生应用的托管方式,[apps.rs](../crates/shell/src/apps.rs) 中的 `AppRegistry::hosting` 在每次启动时据此决定。 -| 启动对象 | 代码路径 | 实际运行方式 | +| 类型 | 打开 | 看什么 | | --- | --- | --- | -| 原生 Rust 模块,如 Reference、Rinx | [module_host.rs](../crates/shell/src/module_host.rs):创建模块,注入作用域句柄、助手和存储接口 | Rust 代码与 UI 在 Shell 进程内运行;每实例有脚本 isolate,Rust 内存仍在同一进程。 | -| 原生进程应用,如受支持桌面上的 Terminal | [clients.rs](../crates/shell/src/clients.rs)、[hub.rs](../crates/shell/src/hub.rs)、[process-apps](../crates/process-apps/src/lib.rs) | 子进程经认证的 Makepad hub socket 交换帧、输入与 AI bus 消息。同一 socket 的 `octos_peer` 封装还支持助手连接(第 5 节);当前随产品提供的进程应用未请求 `agent.octos` 服务。 | -| 隔离脚本应用 | `apps.rs` 进入 App Hub 的 `CARD_MODULE` | Card runner 准入应用包,创建受限的嵌套 Splash VM;capability 限制宿主服务和平台 API。每个包无需编译 Rust 可执行文件。 | -| Glance 卡片 | [glance.rs](../crates/shell/src/glance.rs)、[glance_chat.rs](../crates/shell/src/glance_chat.rs) | L0/L1 `source` 卡片经检查并结合宿主数据 lower 到运行时;Splash `script` 卡片在发布者策略下运行交互逻辑。`notify` 增加通知,UI 用 `glance::dismiss` 移除,应用用 `glance.withdraw` 撤回。聊天通向发布者的 peer。 | - -各产品运行细节见[桌面](../desktop/README.zh-CN.md)、[Home](../phone/README.zh-CN.md)、[ROM](../rom/README.zh-CN.md)和[系统应用](../apps/README.zh-CN.md)。以下构建/启动配方 **unverified(未验证)**,平台前置条件见产品 README。在仓库根目录运行: - -```sh -python3 tools/setup.py --hub /path/to/existing-clones -python3 tools/setup.py --check --cargo -cargo run --release -p octosense -# 可选的进程内 Rust 示例: -MAKEPAD_WM_TEST_APP=reference cargo run --release -p octosense --features app-reference -- --module reference -# 启用内核集成;二进制应来自 Cargo.toml 锁定的 octos 版本: -OCTOS_APP_CORE_BIN=/path/to/pinned/octos \ - cargo run --release -p octosense --features octos-core -``` +| 原生模块(Rinx、Notes、Calculator 等) | [module_host.rs](../crates/shell/src/module_host.rs) | 每个实例一个 Splash 隔离环境。对模块的每次调用都在 `catch_unwind` 下运行(`contain`),所以 panic 只关闭这个应用,不会拖垮 Shell。 | +| 进程应用(源码检出构建中的 Terminal 和 Task) | [clients.rs](../crates/shell/src/clients.rs)、[hub.rs](../crates/shell/src/hub.rs) | hub 只接纳出示了本次启动从 stdin 读到的密钥的子进程 socket;`sandbox_policy` 构建系统沙箱。 | +| 脚本应用(日历、邮件、所有商店应用) | `apps.rs` 中的 `system_card_apps`,然后是 App Hub 的 `CARD_MODULE` | `card` 模块,也就是 Card runner,托管所有系统应用和已安装应用,每个实例一个隔离环境。 | -真实对话还需要在 AI providers 的宿主确认面板配置提供方、模型与凭据。`OCTOS_APP_CORE_BIN` 指定一个文件;此启动器不会搜索 `PATH`。自动 UI 工作使用产品 README 中的隐藏窗口方式。 +`system_card_apps` 生成日历的启动器条目,并在第一次时注册 Shell 的宿主服务(`register_host_services`)。日历自己的窗口不列出任何日程,因为 App Hub 没有可以授予它的 `calendar` 能力;只有它的 Agent 的工具能读到日程。 -脚本应用使用 Design Flow 的 Python CLI `tools/octo`,它封装 App Hub 的 `card-host` 与 `hub`。在 Design Flow 检出目录运行 `tools/octo run /path/to/bundle`(启动配方未验证)。独立 runner 检查隔离 UI 与策略;Mail、Calendar、提供方与 peer 服务由 Shell 注册,因此集成测试需要 Shell。`hub check` 通过说明包满足准入约定。 +运行方法见桌面端 README 的[构建与运行](../desktop/README.zh-CN.md#构建与运行),其中用 `python3 tools/kernel-artifact.py --host --stage target/release` 准备锁定版本的内核。 ## 3. 找到内核的所有者 -依次阅读 [ai-host/src/lib.rs](../crates/ai-host/src/lib.rs)、[kernel/src/lib.rs](../crates/kernel/src/lib.rs)、[launch.rs](../crates/kernel/src/launch.rs)、[kernel.rs](../crates/kernel/src/kernel.rs) 和 [router.rs](../crates/kernel/src/router.rs)。 +只有一个服务拥有内核,所有使用方都连接到它。按以下顺序阅读: -1. Shell 调用 `ai_host::start` 注册服务、配置内核来源。`model.complete` 是一次提供方请求;peer 与工具循环走另一条路径。 -2. 首个获授权的使用方调用 `Core::connect`,得到逻辑 `Connection`;后续使用方共享同一内核代际。 -3. `launch::resolve` 选择桌面子进程(`OCTOS_APP_CORE_BIN` 或显式程序)、Android 打包的 `liboctos.so` 可执行文件,或 OpenHarmony 内嵌服务。iOS 在这里没有内核。 -4. `kernel::supervise` 持有运行中的进程/任务和帧循环。`Router` 在一条物理连接上关联使用方请求 ID 与 session 事件。 -5. OUP 协议使用 JSON-RPC 与异步通知。普通模式经 stdio 传输逐行 JSON;启用 Talk to Octos 后改用宿主管理的回环 WebSocket,客户端共享该内核。 -6. 提供方变化会重启代际,使用方需重连并重新绑定。Peer 记忆的生命周期长于 `Connection`。 +1. **`ai_host::start`**([ai-host/src/lib.rs](../crates/ai-host/src/lib.rs))配置内核,但什么都不启动。它还注册了为脚本应用提供 peer 的 `octos` 服务,以及 `model` 服务,后者做不涉及 peer 和工具的一次性模型调用。 +2. **`Core::connect`**([kernel/src/lib.rs](../crates/kernel/src/lib.rs)):第一个连接启动一个内核代际(generation),之后的连接加入它。最后一个连接断开时内核停止,除非 Talk to Octos 处于打开状态。提供方变化后,`restart` 结束当前代际,使用方重新连接。 +3. **`launch::resolve`**([launch.rs](../crates/kernel/src/launch.rs)):桌面端(`OCTOS_APP_CORE_BIN`,或收据与锁定版本一致的打包 `octos-kernel`)和 Android(`liboctos.so`)上,是运行 `serve --stdio` 的子进程;OpenHarmony 上,是在 Shell 内运行的 `serve_io`;iOS 上没有内核。 +4. **`supervise`**([kernel.rs](../crates/kernel/src/kernel.rs)):每个代际一个任务,持有进程和帧泵送。在任何使用方的帧到达内核之前,它先设定系统 Agent 的精确工具列表(`session/tool_list/set`)。 +5. **`Router`**([router.rs](../crates/kernel/src/router.rs)):所有使用方(比如系统对话和日历的代理)共用一条流。每个请求得到一个内核内唯一的 id,响应只回给发出请求的使用方;每条通知只发给打开了对应会话的使用方。 -ROM 的特权 Android 服务也称“agent”,负责通过 Android bridge 执行允许的平台操作;系统 LLM 对话运行在 octos 中。 +## 4. 跟踪一条系统对话消息 -## 4. 跟踪一条系统聊天消息 +打开 [system_chat/mod.rs](../crates/shell/src/system_chat/mod.rs),然后是 [session.rs](../crates/shell/src/system_chat/session.rs) 和 [link.rs](../crates/shell/src/system_chat/link.rs): -阅读 [system_chat/mod.rs](../crates/shell/src/system_chat/mod.rs)、[session.rs](../crates/shell/src/system_chat/session.rs)、[link.rs](../crates/shell/src/system_chat/link.rs) 和 [system_tools.rs](../crates/kernel/src/system_tools.rs)。 +- `spawn` 启动 `system-chat` 线程,它持有一个 `session::Driver`。UI 线程向它发送 `Command`;对话有变化时,这个线程用 `SignalToUI` 唤醒界面。 +- `Driver` 打开 `SYSTEM_SESSION`(`_main:api:octosense#system`),把每条消息作为 `turn/start` 发出。每次连接后,它都在自己的链路上注册系统 Agent 的宿主工具(不带 `peer` 的 `peer/tools/register`)。 +- `link::poll_for` 用一个会唤醒(unpark)该线程的 waker 轮询内核,所以系统对话不需要 Tokio 运行时。只有在面板打开或有回合运行时,它才保持连接。 -助手面板向工作线程发送 `Command`。`Driver` 打开 `_main:api:octosense#system`、读取历史并启动回合。收到的事件更新聊天模型,`SignalToUI` 唤醒 Makepad 事件循环绘制快照。 +系统 Agent 的内核工具([kernel/src/system_tools.rs](../crates/kernel/src/system_tools.rs) 中的 `SYSTEM_AGENT_TOOLS`)没有一个能读日历,所以它必须去问日历的 Agent。[agents.rs](../crates/shell/src/agents.rs) 中的两个宿主工具帮它做到这一点,它们由系统对话自己应答(`agents::call`): -系统 Agent 是 `_main` profile 的一个 session。宿主通过 `session/tool_list/set` 限定其内核工具;`SYSTEM_AGENT_TOOLS` 包含 `peer_list`、`peer_send_input`、`peer_gather` 和 `peer_respond`。用户开启命令执行后,它通过 Shell 的 `terminal.run` 与审批 UI 执行命令;octos 内置 `shell` 不在其工具列表中。 +- `agents.list` 返回每个有 Agent 的应用、用户是否允许了它,以及它的 peer slug。 +- `agents.ask` 在用户还没决定时显示日历的首次使用面板,并一直挂起这次调用,直到用户回答、peer 准备好;然后返回 slug。 -[agents.rs](../crates/shell/src/agents.rs) 提供两个宿主工具:`agents.list` 查询应用 Agent 及可用状态;`agents.ask` 打开首次同意面板并等待可用的 peer slug(内核为该 peer 分配的标识符)。成功后系统 Agent 用 `peer_send_input` 提交任务;ask 调用只准备访问。首次同意由用户控制。 +系统 Agent 把这个 slug(而不是应用 id `os.calendar`)传给 `peer_send_input`。它的审批交给审批路由,按回合合并;系统对话的面板从不批准任何东西。 -## 5. 一个应用 peer,两条对话通道 +## 5. 准备 peer,给它两条通道 -先读 [app-peers/src/contract.rs](../crates/app-peers/src/contract.rs),再读较大的 [broker.rs](../crates/app-peers/src/broker.rs): +先读 [app-peers/src/contract.rs](../crates/app-peers/src/contract.rs),它就是应用能看到的全部;再读大得多的 [broker.rs](../crates/app-peers/src/broker.rs): -- `OctosAppService`:应用拿到的作用域助手句柄。 -- `OctosContext`:对话/请求句柄;`call(ContextOp, EventSink)` 启动操作;event sink 是接收进度、回复和完成事件(`ContextEvent`)的回调。 -- `ContextSpec`:宿主认证的账号、实例与已授权服务名。 -- `Broker`:将应用请求接到内核的 Shell 适配器,通过 `Arc` 持有连接、peer、context 与在途请求。 +- `OctosAppService`:一个应用实例的作用域服务。`open_conversation` 打开用户的通道,`open_context` 打开私有上下文(Rinx 的小程序)。 +- `OctosContext::call(ContextOp, EventSink)`:一次操作,它的事件以且仅以一个 `Complete` 结束。 +- `ContextSpec`(账号和已授权的服务)与 `TurnTrigger`(是什么启动了这一回合),都由宿主标注([第 6 节](#6-用户在哪里对话))。 -原生模块通过 [hosted.rs](../crates/app-peers/src/hosted.rs) 和 [injection.rs](../crates/app-peers/src/injection.rs) 获得句柄;隔离应用通过 [contained.rs](../crates/ai-host/src/contained.rs) 访问相同 broker。脚本 manifest 声明助手服务或 `agent` 块,或包内包含 `tools.json`,就具备 Agent 资格;运行还需要内核、用户同意及适用的有效账号。 +**日历的 peer。** 脚本应用的 peer 属于 [ai-host/src/contained.rs](../crates/ai-host/src/contained.rs) 中的 `octos` 宿主服务。`contained::prepare`(由 `agents::prepare` 调用)和 `contained::conversation`(由 “Ask Calendar” 面板调用)共用同一个代理 `card.os.calendar`,它由 `hosted::launch`([hosted.rs](../crates/app-peers/src/hosted.rs))构建。日历不区分账号,所以代理代表 `device` 行事。接着,`Broker::ensure_peer`: -原生进程客户端使用 [peer_link](../crates/shell/src/peer_link/mod.rs):`OctosPeer` 消息封装为 `octos_peer`,共用已认证的 hub socket。Shell 根据自己启动的客户端确定应用和实例,检查 `native-apps.json` 的确切 `agent.octos` 授权与同意状态,再把 session、history、turn、interrupt 操作交给该应用的 broker。 +1. 发送 `peer/prepare`,带上记忆命名空间 `app/card.os.calendar/acct-` 和 `resume: true`,对新 peer 还把账号文件夹设为它的工作区(`ToolHost::agent_workspace`); +2. 如果内核没有采用这个命名空间,就拒绝它; +3. 把 peer 的宿主 token 保存在 `PeerRecord` 中,并打开 peer 会话 `…#peer-`,也就是系统 Agent 的通道; +4. 注册应用的工具(`register_tools`,[第 7 节](#7-把工具追到-rust-代码))。注册失败的 peer 不运行任何回合。 -进程内模块也能打开同一客户端,由 `module_host` 认领暂存的连接;注入的 `OctosAppService` 是另一种适配器。 +原生应用开着两个窗口时,只有最早实例的代理驱动 peer(`Broker::drives`;它关闭时由 `take_over` 接替)。日历只有一个代理,总是由它注册工具、接收系统 Agent 的输入。它的 `on_peer_input` 检查账号和同意(`ToolHost::admit_input`),需要拒绝时回复 `peer/input/reject`,否则在 peer 会话上启动这一回合,一次一个。 -进程退出会取消在途工作并关闭请求 context,保留持久 peer;结果不确定的写入返回 `outcome_unknown`。 - -`card.os.news` 是 broker 的应用身份;内核的 peer slug 由 `peer/prepare` 返回,连同 peer session 和宿主凭据一起使用。Peer 按应用与账号区分:无账号应用使用 `device`,Mail 使用宿主报告的登录账号。`ensure_peer` 恢复记录的 peer,核对命名空间并注册工具,然后才运行回合。 - -在 Calendar 例子中,系统委派走系统通道,“Ask Calendar” 走用户通道。图中的**黑板(blackboard)**是内核保存 peer 工作与结果的共享记录,系统 Agent 用 `peer_gather` 读取它。 +**用户的通道。** 每个对话句柄都得到一个新的请求上下文,以 `share_history` 打开(`open_handle`);如果应用声明了 `storage.agent_workspace: "account"`,还会加上 `read_parent`(`context_reads_account`)。 ```mermaid sequenceDiagram - participant H as 用户 - participant S as 系统 Agent session - participant K as octos - participant B as Shell broker - participant A as 应用 peer session - participant C as 应用对话 context - S->>K: peer_send_input(peer, task) - K->>B: peer/input 通知 - B->>B: 检查账号、同意与工具;输入排队 - B->>K: peer session 上 turn/start,携带 input id - K->>A: 执行系统通道回合 - H->>B: Ask app:ContextOp::Turn - B->>K: peer/context/open,share_history;turn/start - K->>C: 执行用户通道回合 - A-->>K: 回合完成 / peer 结果 - K-->>S: 黑板结果可由 peer_gather 读取 - C-->>B: 流式事件与完成 - B-->>H: 应用对话 / 卡片聊天回复 + actor P as 用户 + participant S as 系统 Agent + participant K as octos 内核 + participant B as 日历的代理 + participant L1 as 系统 Agent 的通道 + participant L2 as 用户的通道 + S->>K: peer_send_input(slug, 问题) + K->>B: peer/input + B->>B: 检查账号和同意,输入排队 + B->>K: 在 peer 会话上 turn/start + K->>L1: 运行系统 Agent 的回合 + P->>B: Ask Calendar,TurnFrom 带 TurnTrigger::Person + B->>K: 以 share_history 执行 peer/context/open,再 turn/start + K->>L2: 并行运行用户的回合 + L1-->>K: 回合完成 + K-->>S: 结果写到黑板,用 peer_gather 读取 + L2-->>B: 流式事件和完成 + B-->>P: 在 Ask Calendar 面板中回复 ``` -应用 peer session 是**系统通道**;`open_conversation` 创建带 `share_history` 的请求 context,作为**用户通道**。两者各有记录和回合状态,有限的另一通道近期文本以只读形式补充。多个用户界面可以创建同一 peer 的多个对话 context。`open_context` 用于无历史共享的客户端工作,例如 Rinx 小程序。每个请求 context 是独立的内核 session,归属于原来的应用 peer。 +**原生应用**通过 [peer link](../crates/shell/src/peer_link/mod.rs) 连到同一种代理:进程应用经由它的 hub socket,模块经由它的 `OctosPeer::open` 暂存的 channel(`claim_peer_links`)。Shell 从 socket 或实例确定应用的身份,从不采信帧里的说法。 -多个原生实例共享 peer 时,broker 的 `driver_of` / `take_over` 选择一个 broker 驱动系统通道输入队列,避免多个窗口重复执行 `peer/input`。用户 context 的工作可以与当前系统通道回合并发。 +## 6. 用户在哪里对话 -## 6. 用户在哪里提问,结果返回哪里 +除了系统对话,每个入口打开的都是用户的通道: -| 入口 | 实现与行为 | +| 用户在哪里输入 | 要打开的代码 | | --- | --- | -| 系统助手面板,F8 | `system_chat` 使用系统 Agent 的 session;系统委派工作产生的问题也在这里显示。 | -| “Ask ”,Shift+F8 | [app_chat/mod.rs](../crates/shell/src/app_chat/mod.rs) 经 `agents::conversation` 订阅两条通道并合并历史;发送消息启动用户 context 回合。 | -| 原生应用自己的聊天 | 调用注入的 `OctosAppService::open_conversation`,由应用绘制事件。 | -| 脚本应用自己的聊天 | 经 `host.request` 调用确切获授权的 `octos.session.open`、`octos.session.history`、`octos.turn.start`、`octos.turn.interrupt`。随产品提供的系统应用 Agent 由 Shell 驱动,其脚本无需声明这些调用。 | -| 已发布卡片中的聊天 | `sys.chat` 经 [l0-chat](../crates/l0-chat/src/lib.rs) 与 [glance_chat.rs](../crates/shell/src/glance_chat.rs) 到达发布者的 Agent;宿主核对卡片归属。 | - -手机 Glance 只绘制紧凑摘要,不执行生成 UI。点按使用摘要的屏幕矩形,让 `glance_sheet.rs` 在信息流上方展开至完整可用视口。`card_presentation.rs` 在 Makepad 帧循环中管理可反向切换的动画,不创建 Tokio 任务、不启动应用,也不改变应用 peer。Email 与 Chat 位于同一行。`mail_clip.rs` 提供原邮件和可直接编辑的草稿;`card_chat.rs` 虚拟化消息并将原生输入框固定在键盘上方。两页共享权威草稿。`chat_submit` 校验声明的 `sys.chat` 源、发布者、账户和线程,然后签发绑定已保存修订号的一次性编辑凭证。Mail 通过 `mail.suggest_reply` 消费凭证;没有凭证时只提出建议,不授权发送。关闭展示时保留常驻控件/会话并撤销待批准审核;干净工作区缓存有数量限制,账户或发布失效时清理对应工作区。最终发送仍需实体输入批准准确的宿主审核内容。 - -共享工作区同样承载 Calendar、News、Photos、YouTube 等发布者。声明了 agent 的发布者未提供 `sys.chat` 时,`L0Session::for_card` 会添加宿主拥有的 Card / Chat 会话,不改写 L0 或 Splash 源码。`ContextKind::Card` 携带有大小限制的发布数据及当前 L0 状态,并绑定原发布账户和稳定的卡片线程;它不签发 Mail 编辑凭证,也不新增工具。首次使用可通过 Enable assistant 打开现有权限面板;没有 agent 的发布者不显示 Chat。显式 `sys.chat` 保留原线程。因此内置通知和 Calendar 卡片既可使用原生工作区聊天,也可使用桌面的 “Ask ”。显式 `OCTOSENSE_GLANCE_DEMO=mail` 聊天仍返回预设答案。L0 本地修改和已交互的 Splash 实例不参与干净缓存淘汰;账户失效后不能重开旧发布。 - -绑定草稿的 Mail 回复卡片新增持久化宿主路径:`mail.propose_reply` 创建账户/邮件绑定草稿,`mail.publish_card` 携带其 `draft_id`。`mail_card.rs` 回答 `sys.mail_draft`,按显示中的修订号校验 Field 来源的编辑。`sys.mail_review` 写入只请求 `mail_review.rs` 宿主 UI,不构成批准。`drafts.rs` 拥有不可变尝试和 SMTP 回执,agent 不能直接发送。聊天在已有 Mail peer 中包含绑定邮件、已确认保存的草稿和有限历史。本轮只有经过验证的 Android 实体触摸可批准;桌面/无障碍审批及完整双模型手机验收仍待完成。将新路径视为已验证前,请看[实现与验证表](mail-composable-cards.zh-CN.md)。 +| 系统对话 | `system_chat/session.rs`:系统会话([第 4 节](#4-跟踪一条系统对话消息)) | +| “Ask <app>” 面板 | [app_chat/mod.rs](../crates/shell/src/app_chat/mod.rs):`agents::conversation`,然后是 `ContextOp::TurnFrom { trigger: TurnTrigger::Person }` | +| 原生应用自己的对话(经注入的服务) | `OctosAppService::open_conversation`。Rinx 只用 `open_context`,供小程序的私有上下文使用。 | +| 其他原生应用的对话 | Makepad 的 `OctosPeer`,经由 peer link:不带 `client` 的 `octos.session.open`([peer_link/link.rs](../crates/shell/src/peer_link/link.rs)) | +| 脚本应用自己的对话 | `host.request("octos.turn.start")`,由 `contained.rs` 应答,限于 manifest 声明的名称 | +| 卡片的 Chat 标签页,或声明了 `sys.chat` 的卡片 | [glance_chat.rs](../crates/shell/src/glance_chat.rs) 和 [l0-chat](../crates/l0-chat/src/lib.rs) | +**卡片工作区**([卡内对话](../README.zh-CN.md#卡内对话))。[glance_sheet.rs](../crates/shell/src/glance_sheet.rs) 显示打开的卡片:手机上全屏,桌面端居中。如果发布者有 Agent,而卡片没有声明对话,`L0Session::for_card`([glance_card.rs](../crates/shell/src/glance_card.rs))会在 Card / Chat 标签页后面加上宿主拥有的 `WorkspaceChat`。`chat_submit` 把每一轮经由 `glance_chat::perform_bound` 发往发布这张卡片的账号(`agents::conversation_for_account`),卡片的数据和本地状态只作为上下文(`ContextKind::Card`),从不作为工具。邮件回复卡片则用 Email / Chat 共用一份保存的草稿;Chat 的每一轮都带着一个一次性令牌(`drafts::issue_chat_edit`),让 `mail.suggest_reply` 能保存这次修改([可组合的邮件卡片](mail-composable-cards.zh-CN.md))。 -手机触控导航尚无打开 Ask-app 面板的对应控件。应用自己的聊天与卡片聊天仍是独立入口。 +尚未实现:还没有随附的应用从自己的界面打开用户的通道;入口是 Shell 的面板和卡片。 -不同入口的停止操作影响不同通道: +触发方式决定了审批在多大程度上信任一个回合。只有 Shell 自己的界面(“Ask <app>” 面板和系统对话)才会标注 `TurnTrigger::Person`;进程内模块本可以经注入的服务这样做,但目前都没有。应用说 `"trigger": "person"` 时,会被记为 `AppSaysPerson`(`TurnTrigger::from_args`),中转把它作为应用发起的运行交给审批路由(`host_tools/relay.rs` 中的 `trigger_of`);卡内对话也得到同样的标注。不带触发方式的 `ContextOp::Turn`(Rinx 的小程序就这样发送)是 `Unknown`,常设规则会跳过它。邮件的新邮件事件([agent_events.rs](../crates/shell/src/agent_events.rs))以 `TurnTrigger::Incoming` 在这条通道里运行;其他应用还没有事件。 -| 操作 | 中断范围 | -| --- | --- | -| Ask-app 面板的 Stop | 用户通道 | -| “Stop the system agent’s task” | 系统通道 | -| 底层 `ContextOp::Interrupt` | 可以中断两条通道 | - -隐藏面板保留 context 和订阅;切换应用或撤销访问会关闭它们。 - -Shell 将聊天输入动作标为 `TurnTrigger::Person`。脚本调用 `octos.turn.start` 可传 `trigger` / `from`,但 `trigger: "person"` 被记录为 `AppSaysPerson`;脚本标签不能作为可信用户动作来匹配审批规则。 +对于 “Ask Calendar”,回复经由上下文的 `EventSink` 流回,面板的跟随者(`OctosContext::subscribe`)能收到两条通道的事件。尚未实现:脚本应用收不到推送的事件,它的 `octos.turn.start` 返回的是完整的回复。 -结构化工具结果回到发起它的应用回合。用户 context 的最终事件经其 event sink 回到对应界面;系统委派回合的结果写入 peer 黑板,由系统 Agent 获取并整理回答。 +停止按通道进行:面板上的“停止”只结束用户的回合(`app_chat::stop`),系统 Agent 的回合有自己的控件(`stop_system_agent`)。对一段对话执行 `ContextOp::Interrupt` 会同时结束两条通道,因为设备归用户所有。 -## 7. 从工具声明追到 Rust 执行器 +## 7. 把工具追到 Rust 代码 -阅读 [host_tools/script_apps.rs](../crates/shell/src/host_tools/script_apps.rs)、[relay.rs](../crates/shell/src/host_tools/relay.rs)、[app-peers/host_tools.rs](../crates/app-peers/src/host_tools.rs)。以 Calendar 的 `calendar.events` 为例: +从声明一直跟到它读取的文件,看 `calendar.events` 走过的路: -1. [tools.json](../apps/calendar/bundle/tools.json) 声明 schema、风险、共享与实现方式;App Hub 校验应用包并计算摘要。 -2. `script_apps::from_bundle` 加载准入的声明;`install` 加入 relay catalog,并安装 `HostServiceExecutor`。 -3. 驱动 peer 的 broker 用 `peer/tools/register` 注册精确工具列表;模型才能请求这些工具。 -4. octos 发出 `peer/tool/call`,broker 盖上真实调用方、账号与 context,交给 Shell 的 `ToolHost`。 -5. `Relay::handle` 检查授权、同意、账号、输入 schema、大小与调用预算;需要审批的调用进入下述路由器。 -6. 执行器以**工具所属应用**的身份调用 Calendar 宿主服务,读取 [Calendar 存储](../apps/calendar/host-service/src/lib.rs)。结果经回复队列返回,检查输出 schema/大小后,`ToolReply` 最多发送一次 `peer/tool/result`。 -7. 模型消费 JSON 结果并继续回合,输出回答或请求另一个获授权工具。 +1. **声明**于 [apps/calendar/bundle/tools.json](../apps/calendar/bundle/tools.json):它的 schema、`risk: "read"`、`implemented_by: "host-service"`,没有标为 `shareable`。 +2. **加载**:[host_tools/script_apps.rs](../crates/shell/src/host_tools/script_apps.rs) 中的 `from_bundle` 通过 App Hub 会核对摘要的加载器读取它。`install` 把工具加入中转的目录,并为 `os.calendar` 安装一个 `HostServiceExecutor`。 +3. **注册**:`broker.rs` 中的 `register_tools` 用 `ShellToolHost::declarations`([host_tools/mod.rs](../crates/shell/src/host_tools/mod.rs))返回的声明进行注册。 +4. **调用**:octos 在注册它的那条链路上发送 `peer/tool/call`。代理把账号、上下文和调用方标注进一个 `HostToolCall`([app-peers/src/host_tools.rs](../crates/app-peers/src/host_tools.rs)),由 `ShellToolHost::tool_call` 排队交给 UI 线程。 +5. **检查**:`Relay::handle`([relay.rs](../crates/shell/src/host_tools/relay.rs))检查授权、同意、账号是否已退出登录、参数大小和 `input_schema`,然后是调用方的预算(默认每轮 32 次、每天 1000 次)。 +6. **运行**:`HostServiceExecutor::execute` 以 `os.calendar` 的身份,把一个 `ServiceCall` 分派到 App Hub 的服务注册表,不弹出任何面板。`CalendarService::call`([apps/calendar/host-service/src/lib.rs](../apps/calendar/host-service/src/lib.rs))读取 `/.host/calendar/events.json`,按 `from`、`to` 和 `limit` 过滤。 +7. **应答**:`script_apps::poll` 从 App Hub 的队列取回回复,`checked_reply`(在 `relay.rs` 中)按 `output_schema` 和大小上限检查它,`ToolReply` 只发送一次 `peer/tool/result`。 -`implemented_by: "host-service"` 需要存在可调用的服务;`implemented_by: "app"` 虽可作为元数据准入,但 Card runner 尚无脚本工具执行器,会返回 unavailable。声明文件和准入检查分别描述、验证接口;真正操作需要执行器代码。 +`calendar.events` 只读,所以不会询问任何人。`calendar.remove_event`(`destructive`、`confirm: host`)则先由 octos 把关:内核发起一个 `host_tool` 审批,代理把它交给 `ToolHost::host_tool_approval`,只有获批的调用才会到达。 -[host_tools/mod.rs](../crates/shell/src/host_tools/mod.rs) 将 broker 线程的调用放入 inbox,由 UI 事件循环泵送并排队回复: +中转按工具的所有者分派每一次调用: -| 工具路线 | 执行边界 | +| 所有者 | 执行器 | | --- | --- | -| 隔离应用,`implemented_by: "host-service"` | `HostServiceExecutor` 调用准入的所属应用 Rust 服务。 | -| 原生模块 | `OctosAppService::set_tool_executor` 注册的回调。 | -| 原生进程 | `peer_link` 经认证的 hub socket 发送工具请求;仍需注册与授权。 | -| 系统 `terminal.run` | 开启 Command execution 后,Shell 经 AI bus 在可见 Terminal 中输入已批准的命令。 | -| `files.list/read/search` | Unix 上的 Shell 执行器,范围为调用方允许访问的账号工作目录。 | -| `dev.run` | Shell 执行器,仅提供给开发者模式覆盖的 peer。 | -| Toolbox 工具 | 已注册的工具箱执行器/工作流,需要 `toolbox-peers` feature 与应用授权。 | +| 脚本应用,`implemented_by: "host-service"` | `HostServiceExecutor`:应用的宿主服务(`calendar`、`mail`、`news`),或者为 `.notify` 服务的 Shell 通知服务 | +| 脚本应用,`implemented_by: "app"` | 尚无:调用以 `app_tool_unavailable` 被拒绝 | +| 原生应用 | 它已打开的实例:经由 peer link 上的 `OctosPeer::serve_tools`,否则经由它的 AI bus 服务(应用没打开时回答 “Open … first”)。用 `OctosAppService::set_tool_executor` 安装的执行器优先。 | +| `terminal.run`(仅系统 Agent) | 先由面板展示确切的命令,再经 AI bus 输入到可见的 Terminal | +| `files.list`、`files.read`、`files.search`、`dev.run` | Shell 自己 | +| 工具箱(`toolbox-peers` feature) | 工具箱的执行器 | ### 审批顺序 -首次 Agent 同意、工具授权和逐次调用审批是三个独立检查。[approvals/router.rs](../crates/shell/src/approvals/router.rs) 按以下顺序处理审批请求: +首次同意、工具授权和逐次调用的审批是相互独立的检查。对一次审批,[approvals/router.rs](../crates/shell/src/approvals/router.rs) 中的 `Router::request` 依次尝试: -1. 外部客户端的回合由该客户端处理;Shell 不回答其提示,也不使其过期。 -2. 开发者模式自动批准覆盖应用的请求,包括 `auto_approvable: false` 和 `confirm: app`。 -3. `confirm: app` 交给工具所属应用注册的确认面板,显示调用方;常设规则不会回答。若应用未在 `app_wait_s` 内注册面板,请求被拒绝。 -4. `auto_approvable: false`、结果未知,以及被标为外部连接的调用,要求用户决定。 -5. 用户预先设置的常设规则可决定符合条件的请求;由收到的内容触发的运行默认跳过,除非规则明确包含。 -6. 其余请求显示 Shell 确认面板;同一系统任务的调用可以合并展示。 +1. 外部客户端自己的提示,用于该客户端的回合; +2. 开发者模式,用于它覆盖的应用; +3. 所属应用自己的面板,用于 `confirm: app` 的工具(应用没有注册面板时,`app_wait_s` 即 120 秒后拒绝); +4. 实时面板,用于必须每次都问的调用:`auto_approvable: false`、结果未知、来自外部连接; +5. 用户的常设规则; +6. 否则由 Shell 面板展示确切参数。 -决定只交付一次,并记录到仅所有者可读写的审计日志;自动决定还产生通知。Shell 持有的宿主连接待答请求在 `OCTOSENSE_PROMPT_DEADLINE_SECS`(默认十分钟)后以拒绝过期。宿主工具审计保存参数摘要而非原始参数。模型的文本回复或 `peer_respond` 无法批准工具调用。 +决定会写入 `logs/approvals-audit.jsonl` 审计日志;时限就是 `app-peers/src/host_tools.rs` 中的 `DEFAULT_PROMPT_DEADLINE`(10 分钟)和 `EXPIRY_GRACE`(30 秒)。系统 Agent 不能批准:octos 拒绝它用 `peer_respond` 回答审批。每一步的含义见 [architecture.zh-CN.md 第 5 节](architecture.zh-CN.md#5-审批)。 -## 8. “访问应用数据”对应哪些存储 +发送邮件从不经过这个审批路由。`mail.propose_send` 只准备确切的邮件内容;卡片里宿主自己的审阅界面(`mail_review.rs`)只有在用户亲手触摸之后(`trusted_user_gesture`,目前只在 Android 上)才调用 `drafts::approve_and_send`,开发者模式不能代替这一步。 -| 数据 | 应用 Agent 的访问路线 | -| --- | --- | -| 应用账号目录 | [app_storage](../crates/shell/src/app_storage/mod.rs) 与 `ToolHost::agent_workspace` 将新 peer 的 cwd 绑定到允许的目录;内核文件工具仍需授权。 | -| 请求 context 中读取账号文本文件 | [files.rs](../crates/shell/src/host_tools/files.rs) 的 `files.list/read/search`,当前限 Unix,要求同意和可用工作目录;限制大小,拒绝符号链接穿越,隐藏同级 context。SQLite 数据需另设服务接口。 | -| 用户对话的父账号目录 | 只有 `storage.agent_workspace: "account"` 允许时才请求 `read_parent`。父目录只读,写入留在 context 自己的目录;普通客户端 context 不自动获得此访问。 | -| 宿主服务数据库或远程账号 | 通过该服务实现的具体工具;工作目录权限不包含所有宿主数据库与远程凭据。 | -| Agent 记录与记忆 | octos session/context 和 `app//acct-` 命名空间,与应用业务数据分别存放。 | -| 机密 | 宿主机密存储和确认面板;不进入 Agent 工作目录或脚本状态。 | - -对 Calendar 问题,执行器通过 `calendar.events` 读取宿主目录中的 `calendar/events.json`。模型收到服务结果,无需自行打开文件。其他应用的工具范围更窄: +## 8. 数据存放在哪里 -| 应用 | 当前数据/工具边界 | -| --- | --- | -| News | `news.list`、`news.read`、`news.notify` | -| Mail | 绑定账户的 accounts/folders/sync/list/peek、notify/publish_card/skip_event,以及 propose_reply/draft/suggest_reply/propose_send;不暴露发送、批准或凭据工具 | -| Photos、Maps、Camera、YouTube | 仅各自的 `.notify`,经 [glance_notice.rs](../crates/shell/src/glance_notice.rs) 发布共享通知卡片 | -| AI providers | 没有应用 Agent | +应用的数据分几处存放,每处各有一个所有者: -旧 AppCard 个人数据导入器不会自动同步当前 Mail 存储。 - -账号目录名使用 SHA-256 派生 tag,peer 记忆名称使用 broker 的 FNV 派生 tag;它们是不同的兼容标识。退出登录会暂停访问并保留数据;移除账号/卸载还会请求 `peer/purge`,忙碌时重试。恢复 peer 时,其已保存的工作目录不能悄悄改变。 +| 数据 | 位置 | Agent 如何访问 | +| --- | --- | --- | +| 账号文件夹 | `apps//accounts//`,或 `accounts/device/`([app_storage/mod.rs](../crates/shell/src/app_storage/mod.rs)) | 它是 peer 的工作区,在 peer 创建时固定。用户的通道运行在自己的 `contexts//` 中,通过 `read_parent` 或宿主的 `files.*` 工具读取账号文件夹([files.rs](../crates/shell/src/host_tools/files.rs):仅限 Unix;每次读取最多 128 KiB,列目录最多 500 项,搜索最多 100 条匹配)。 | +| 宿主服务的数据 | App Hub 的宿主目录 `/.host/`:日历的日程,邮件的邮件和回复草稿(`drafts.rs`) | 只能通过该服务的工具访问;任何工作区都不包含它。 | +| 对话记录和记忆 | octos 中,命名空间 `app//acct-` | 归 Agent 自己。`` 是 FNV-1a 哈希(`account_tag`);文件夹名则是另一种哈希,SHA-256(`account_hash`)。 | +| 机密 | 应用机密:macOS 和 iOS 上存入钥匙串(索引在 `/secrets//`),其他平台是该文件夹中的文件。提供方密钥:macOS 上存入钥匙串,其他平台存入内核核心目录下的文件(除 Windows 外仅所有者可读) | 永远不能访问。`app_storage::check` 会拒绝任何包含或链接到机密的工作区。 | -## 9. 跨应用工作与求助 +`agent_workspace_in`([host_tools/mod.rs](../crates/shell/src/host_tools/mod.rs))把账号文件夹交给每个拥有 `octos.*` 服务的原生应用,即使它声明了 `storage.agent_workspace: "none"`;也交给每个没有这样声明的脚本应用。日历的 Agent 得到 `apps/os.calendar/accounts/device/`,但日程不在那里:`calendar.events` 从 `.host/calendar/events.json` 读取日程,再把 JSON 交给模型。 -**委派任务:**系统 Agent 用 `peer_send_input` 请求现有应用 peer 工作,再获取结果。Shell 驱动该应用回合,peer 身份可以跨进程生命周期保留。 +退出登录会挂起 peer,之后它的调用都会得到 `signed_out`。删除账号或卸载应用会用 `peer/purge`([purge.rs](../crates/app-peers/src/purge.rs))清除它。 -**直接调用另一应用的 API 工具:**这条路线直接调用所属应用的执行器,无需经过它的模型。例如,另一应用要调用可共享的 Calendar 工具,必须满足以下条件: +## 9. 跨应用协作与求助 -1. 工具已有声明、`shareable: true` 和实际执行器。 -2. 调用方有授权。脚本 manifest 在 `agent.tools` 中请求带点的名称,原生应用使用审核过的授权;`may_call` 检查这些规则,显式开发者模式例外另行处理。 -3. 对脚本包,App Hub 准入(宿主接受应用包的检查)通过 `HostLimits.offered_tools` 允许该名称。 -4. `Catalog::owner_of` 能找到所属应用,relay 才能调用其执行器。 +有三种操作容易混淆。 -接收方应用的数据访问由执行器控制。当前 owner 解析覆盖原生命名空间、toolbox 和 `os.`,尚不支持发现任意已安装商店应用。默认准入未提供 `mail.send` 等任意名称,Mail 当前也未实现这个 Agent 工具;仅有 relay 路线不足以使它可调用。 +**委派。** 系统 Agent 用 `peer_send_input` 请应用的 Agent 做事([第 4 节](#4-跟踪一条系统对话消息)),由 Shell 在现有的 peer 上启动那一回合。不会启动新进程。 -**提问或使用系统设施:**应用 Agent 可调用获授权的 `ask_user_question`。[questions/mod.rs](../crates/shell/src/questions/mod.rs) 按回合来源路由:系统委派工作在系统聊天中提问,用户/应用回合在应用对话中提问,由用户在 Shell 界面回答。系统设施以明确授权的工具提供,例如 [toolbox](../crates/toolbox/README.md) 工作流。通用的应用到系统 Agent 对话 RPC 尚未实现。`peer_respond` 处理 peer 协作;审批消息使用单独的宿主回答句柄。 +**直接调用另一个应用的工具。** 这会绕过拥有该工具的应用的模型,所以只有以下条件全部满足时,中转才允许: -## 10. 映射到 Rust 的实际执行模型 +1. 所有者把工具声明为 `shareable: true`,并且有对应的执行器。 +2. 调用方获得了授权:脚本应用在 `agent.tools` 中写上带点的名称,原生应用在 `native-apps.json` 条目的 `agent.grants` 中声明。`Catalog::may_call` 检查这一点。 +3. 对脚本应用包,App Hub 的准入提供了这个名称(`HostLimits.offered_tools`)。 +4. `Catalog::owner_of` 能从命名空间找到所有者:工具箱、同名的原生应用,否则是系统应用 `os.`。 -Peer 是持久身份与状态。**Tokio task** 是由 runtime 工作线程调度的异步计算;等待 I/O 时,工作线程可以运行其他就绪任务。一个回合会用到多个 task,peer 在它们结束后仍存在。 +日历的工具没有一个可共享,所以其他 Agent 只能去问日历的 Agent;新闻共享了 `news.list` 和 `news.read`。尚未实现:`owner_of` 从不解析到商店应用,所以商店应用还不能共享工具。 -沿着直接发送给 “Ask Calendar” 的问题,依次跨过这些边界: +**求助。** [questions/mod.rs](../crates/shell/src/questions/mod.rs) 按回合的来源路由 Agent 的 `ask_user_question`:`peer/input` 回合的问题进系统对话,其余的进应用的对话。只有用户能回答,而且只能在 Shell 界面上回答。系统设施只以获授权的工具的形式提供给应用的 Agent,例如[工具箱](../crates/toolbox/README.md)的工作流。尚未实现:应用不能与系统 Agent 发起对话,`OctosAppService` 没有这样的调用。 -1. Makepad UI 将 context 请求交给 Calendar 的 broker。 -2. Broker 的 runtime 经共享内核 `Connection` 发送 `turn/start`,由内核服务 supervisor 送入传输连接。 -3. octos spawn 回合编排 task;它先在启动屏障等待活跃回合注册成功,再开始 Agent 处理。 -4. 若模型调用 `calendar.events`,工具 future 等待 broker 与 Shell relay 将请求交给 Calendar 宿主服务。返回的数据让模型继续;回复事件经用户 context 返回并唤醒 UI。 +## 10. 映射到 Rust 的实际执行模型 -这些是调度边界;每个 peer 并不各自创建线程或进程。下表列出各部分的所有者: +peer 是存储的状态,回合是 octos 中的一组 Tokio 任务;线程属于服务,而不属于 Agent([为什么它省内存、省算力](../README.zh-CN.md#为什么它省内存省算力))。 -| 层 | 实际执行模型 | 源码 | +| 层 | 如何运行 | 去哪里看 | | --- | --- | --- | -| Makepad Shell | UI 事件循环,绘制、事件分发、宿主工具 relay 泵送 | `module_host.rs`、`host_tools/mod.rs` | -| 系统聊天 | 普通 `std::thread`,交换命令与快照;`link::poll_for` 用 waker/unpark 轮询内核接收 | `system_chat/mod.rs`、`link.rs` | -| Shell 内核服务 | 惰性创建 Tokio runtime:**2 个工作线程,8 MiB 栈**;每代一个 supervisor task 持有传输与进程生命周期,另有 I/O task | `kernel/src/lib.rs::Inner::runtime`、`kernel.rs::supervise` | -| 应用 broker | **每次 `Broker::new` 创建 1 个工作线程的 runtime**,运行连接泵送、请求 future、重试和截止任务;多个 broker 可共享一个 peer | `app-peers/src/broker.rs` | -| OpenHarmony 内嵌内核 | 在宿主 runtime 上 spawn `serve_io`,经 `tokio::io::duplex` 通信 | `kernel.rs::start` | -| octos OUP 回合 | 已 spawn 的编排任务先在 `oneshot` 启动屏障等待活跃回合准入;成功后 `run_standalone_turn` 再 spawn Agent 处理与辅助任务 | 锁定 octos 的 `crates/octos-cli/src/api/ui_protocol_transport.rs` | -| octos 传输出口 | WebSocket 使用异步 writer task;内嵌/stdio 使用有界同步队列和普通 writer 线程 | 同一 octos 文件 | -| 宿主服务/文件执行器 | 按服务使用 UI 泵送、回调/回复队列或处理阻塞操作的工作线程 | `host_tools/files.rs`、App Hub `services.rs`、各应用服务 | +| Shell 界面 | Makepad 的 UI 线程:绘制、事件,以及运行中转的 `host_tools::pump` | `lib.rs`、`host_tools/mod.rs` | +| 系统对话 | 一个 `std::thread`,用 `link::poll_for` 轮询内核 | `system_chat/mod.rs`、`link.rs` | +| 内核服务 | 一个首次使用时才创建的 Tokio 运行时:2 个工作线程,8 MiB 栈;每个代际一个 supervisor 任务 | `kernel/src/lib.rs` 的 `Inner::runtime`、`kernel.rs` 的 `supervise` | +| 应用代理 | 每次 `Broker::new` 创建一个运行时,1 个工作线程:链路循环、请求、重试、时限 | `app-peers/src/broker.rs` | +| 宿主服务 | 由 App Hub 的 `services::dispatch` 在调用方的线程上调用:工具调用时就是 UI 线程,日历在那里应答。邮件(`work` 线程、`mail-fetch`)和新闻(`news-fetch`)把网络工作放到自己的线程上。 | `script_apps.rs`、`apps/*/host-service/` | +| 桌面端和 Android 上的 octos | 独立进程,使用 Tokio 的默认运行时:每个 CPU 核心一个工作线程(`ServeCommand::execute`) | octos 的 `crates/octos-cli/src/commands/serve.rs` | +| OpenHarmony 上的 octos | `serve_io` 运行在内核服务的运行时上,经由 `tokio::io::duplex` 通信 | `kernel.rs` 的 `start` | +| 一个 octos 回合 | 一个 spawn 出来的任务,先在 `oneshot` 启动屏障处等待,然后运行 `run_standalone_turn` 及其自己的任务 | octos 的 `crates/octos-cli/src/api/ui_protocol_transport.rs` | ```mermaid flowchart LR - UI["Makepad UI 线程"] --> CMD["系统聊天 std 线程"] - UI --> B["Broker runtime:请求与连接任务"] - CMD --> C["Kernel Connection 通道"] + UI["Makepad UI 线程
中转、宿主服务"] --> CMD["system-chat 线程"] + UI --> B["代理运行时,1 个工作线程
链路循环、请求"] + CMD --> C["内核 Connection 的 channel"] B --> C - C --> SUP["内核服务 supervisor task"] + C --> SUP["内核服务运行时,2 个工作线程
代际 supervisor 任务"] SUP <-->|"stdio 或宿主 WebSocket"| OUP["octos 协议分发器"] - OUP --> T1["系统通道回合 task"] - OUP --> T2["用户 context 回合 task"] - T1 --> TOOL["工具 future 等待宿主回复"] - TOOL --> B - B --> Q["Shell relay 队列"] - Q --> UI + OUP --> T1["系统 Agent 的通道
回合任务"] + OUP --> T2["用户的通道
回合任务"] + T1 --> TOOL["工具 future 等待
peer/tool/result"] + T2 --> TOOL + TOOL -->|"peer/tool/call"| B + B -->|"host_tools::submit"| UI ``` -`mpsc` 是多发送方邮箱,`oneshot` 交付一次关联回复,`watch` 保存最新的生命周期/就绪状态。Broker 的请求 ID 对应 `oneshot` sender;连接循环用 `tokio::select!` 同时处理进出消息。内核 supervisor 选择控制消息、内核输出与进程退出。`Arc` 共享所有权,`Weak` 避免延长已关闭 broker/context 的生命周期,代际/epoch 检查拒绝旧账号或连接的迟到回复。 +对于 “Ask Calendar”,octos 的 `handle_turn_start_with_accept` spawn 出用户的回合任务;它的 `calendar.events` future 一直等待,直到调用在 UI 线程走完一个来回。 + +三种 channel 反复出现:`oneshot` 传递一个回答(请求的回复、回合的启动),`mpsc` 是邮箱(supervisor 的 `Ctl` 消息),`watch` 保存最新状态(代际是否就绪)。代理的链路循环和 supervisor 都是 `tokio::select!` 循环;账号代际和 `link_epoch` 检查会丢弃发给旧账号或旧连接的回复。 -Broker 同步 `bind` / `host_request` 会等待通道回复,不应在绘制回调中调用。不同 session 的异步模型 I/O 可重叠,同步文件工作仍走已有的工作线程边界。每应用/账号一个 peer 是身份规则;task 和线程数量由上表的 runtime 与活跃操作决定。 +`Broker::bind`、`Broker::host_request` 和 `OctosAppService::prepare` 会阻塞调用方最多一分钟,所以不要在 UI 线程上调用它们。 -## 11. 测试与尚待实现的部分 +## 11. 测试 -[Broker 测试](../crates/app-peers/tests/broker.rs)提供可执行协议示例:`a_persons_message_runs_while_the_system_agents_input_runs`、`a_lane_stop_leaves_the_other_lane_running`、`a_kernel_without_shared_history_is_refused_for_the_conversation`、`removing_an_account_purges_its_recorded_peer_and_drops_the_record`。[Shell relay 场景测试](../crates/shell/src/host_tools/scenario_tests.rs)覆盖调用方、工具与审批边界;修改对话或执行路径时从这些测试开始。 +[crates/app-peers/tests/broker.rs](../crates/app-peers/tests/broker.rs) 让代理对接一个脚本化的内核,它的测试名本身就写明了协议的规则。可以先看 `a_persons_message_runs_while_the_system_agents_input_runs`、`a_lane_stop_leaves_the_other_lane_running`、`a_kernel_without_shared_history_is_refused_for_the_conversation` 和 `removing_an_account_purges_its_recorded_peer_and_drops_the_record`。[host_tools/scenario_tests.rs](../crates/shell/src/host_tools/scenario_tests.rs) 用锁定版本的 octos 和一个脚本化的模型,用一个测试用的新闻应用端到端地跑完同样的两条通道场景;运行方法写在它的文件头部。 -在仓库根目录运行单元测试与脚本化连接器测试: +在仓库根目录运行单元测试和脚本化连接器测试: ```sh cargo test --locked -p octosense-kernel -p octosense-app-peers \ --features octosense-app-peers/octos-core,octosense-app-peers/ws ``` -可选真实内核测试在缺少二进制环境变量时提前返回;将结果当作集成证据前,阅读 [app-peers 测试说明](../crates/app-peers/README.md)。可见 UI、真实提供方对话和设备行为需要分别运行。 +真实内核测试(`crates/app-peers/tests/real_kernel.rs` 和场景测试)在没有设置内核二进制时会打印一条说明并直接通过,所以测试全部通过不能当作集成证据;[app-peers README(英文)](../crates/app-peers/README.md#testing)说明了如何给它们提供内核。可见界面、真实提供方和设备上的行为需要另外运行。 -Mail 已有可选的新邮件事件调度器,已接纳的 `AGENT.md` 和技能文本会随每轮注入,系统 Agent 可配置账户级补充指令。见[邮件事件导读](mail-agent-events.zh-CN.md)。通用触发器/cron、内核原生技能安装、脚本实现的 Agent 工具分发,以及通用应用到系统 Agent 对话 RPC 仍待实现。 +其他仓库的导读(固定版本):[Design Flow](https://github.com/OctoSense-org/OctoScript-App-Design-Flow/blob/218b25d2460d64f843932f67d419467618464fb9/docs/CODE-WALKTHROUGH.md)、[App Hub](https://github.com/OctoSense-org/OctoSense-App-Hub/blob/0d5b47a2ae9eb98020feca26b7c895a3cf797dc1/docs/CODE-WALKTHROUGH.md)、[Octoscript-Makepad](https://github.com/OctoSense-org/OctoScript-Makepad/blob/2cc5ef37d7d6a3d2992673389ce74488f7bb2d87/docs/architecture-walkthrough.md) 和 [octos](https://github.com/octos-org/octos/blob/056173e85b150e387805fc307fe231064ac1ed35/docs/octosense-integration-walkthrough.md)。 diff --git a/docs/architecture.md b/docs/architecture.md index 80c06a82..32a4f0ce 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -2,17 +2,9 @@ English | [简体中文](architecture.zh-CN.md) -How OctoSense fits together: processes, agents, tools, approvals, storage and trust boundaries. Follow the [code walkthrough](architecture-walkthrough.md) for calls and ownership. Dependency versions come from [Cargo.toml](../Cargo.toml) and [native-runtime.lock.json](../native-runtime.lock.json); the dated design notes below record earlier decisions. +This is the full picture of how OctoSense is built, with the code behind each part. It assumes the README's [Key concepts](../README.md#key-concepts) and [How it fits together](../README.md#how-it-fits-together). [ai-services.md](ai-services.md) covers what each kind of app can call and how to run the AI services locally, and the [code walkthrough](architecture-walkthrough.md) reads the code in order. -**Mail reply implementation:** [ADR0007](adr/0007-composable-mail-action-cards.md) now has draft/editor/chat/review adapters in source. Mail agents can propose drafts and review, but cannot approve or send. Even developer mode requires the host-owned review control; currently only physical Android touch can authorize it. The [current code and verification gates](mail-composable-cards.md) supersede older Mail demo/send-path descriptions below. Scoped OnePlus 6 tests with DeepSeek and MiniMax are recorded there; the full device/UX matrix and desktop/accessibility send approval remain incomplete. - -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, and Implemented on 2026-09-29 apart from its follow-ups). 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. +The decisions behind it are [ADR 0001](adr/0001-one-octosense-repository.md) (one repository), [ADR 0002](adr/0002-event-driven-app-agents.md) (event-driven app agents; Proposed, partly built), [ADR 0003](adr/0003-shared-octos-client-access.md) (Talk to Octos) [ADR 0004](adr/0004-native-apps-hosting-and-peers.md) (native apps, app agents, cross-app work and approvals) and [ADR 0007](adr/0007-composable-mail-action-cards.md) (Mail cards with drafts, chat and host-approved sending; in progress). The text describes the code as it is; what is not built yet is marked **Not yet** or **Planned**. ## Contents @@ -34,14 +26,14 @@ The decisions behind it are [ADR 0001](adr/0001-one-octosense-repository.md) (on ![OctoSense AI services: one granted cross-app call](images/ai-services-cross-app-call.png) -*Illustrations (generated). Solid lines are on main; dashed ones are planned or in progress (the peer link for process apps has been on main since [#130](https://github.com/OctoSense-org/OctoSense/pull/130), and the Terminal uses it; `peer/input` and host tools are on main since the octos 665209e5 pin, octos#2567). Where a picture and the text below differ, the text is authoritative: for example, `peer_send_input` puts the message in the peer's inbox, and the peer takes it as its next turn.* +
Text version (Mermaid) ```mermaid flowchart LR person(["The person"]) subgraph shellp["OctoSense shell process (desktop, or Home on a phone)"] wm["Window manager, launcher, sheets,
system chat, glance panel
crates/shell"] - mods["In-process native modules
App Hub, Rinx, (AppCard)"] + mods["In-process native modules
App Hub, Rinx, Notes, Clock, …, (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"] @@ -49,7 +41,7 @@ flowchart LR svc["App host services
mail, calendar, news; notices"] bus["AI services bus
crates/shell/src/ai_bus.rs"] end - term["Process app (desktop)
Terminal"] + term["Process apps (desktop)
Terminal, Task"] subgraph kern["octos kernel (one per shell)"] sys["System agent session
(profile _main)"] peers["App peers
one per (app, account)"] @@ -59,7 +51,8 @@ flowchart LR wm --- mods wm --- runner term <-->|"hub (loopback WebSocket)"| wm - mods -- "injected OctosAppService" --> aihost + term -- "peer link, on the hub socket" --> aihost + mods -- "injected OctosAppService or peer link" --> aihost runner -- "host.request(octos.*)" --> aihost aihost <-->|"OUP over stdio
(or host-token WebSocket)"| kern sys -- "peer_send_input / blackboard" --> peers @@ -73,92 +66,73 @@ flowchart LR 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) | -| host-tool relay -> app host services (mail, calendar, news, notices) | -| 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) | - | turns and tool dispatch | - Talk to Octos clients ---------> +-----------------------------------------+ - (external token, allowlist, opt-in) -``` +
+ +Every agent is a session in the one kernel, and every call from an agent to an app's tools, like every approval, passes through the shell. [Section 8](#8-worked-example-emailing-a-meeting-invite) follows the call in the second picture. ## 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. +There is one shell process per device: the desktop (`desktop/`, package `octosense`) or Home on a phone (`phone/`, package `octosense-home`), both built from `crates/shell` ([ADR 0001](adr/0001-one-octosense-repository.md)). It 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`): +The kernel is a shell service, at most one per shell, in [`crates/kernel`](../crates/kernel/README.md) (package `octosense-kernel`), reached through [`crates/ai-host`](../crates/ai-host/README.md). `launch::resolve` (`crates/kernel/src/launch.rs`) decides how it runs: -| Platform | Kernel | How it is started | -| --- | --- | --- | -| Desktop (macOS; Windows and Linux untested) | a child process: the embedder's `Options::program`, else the `octos` binary named by `OCTOS_APP_CORE_BIN`, else the packaged `octos-kernel` beside the shell (run only when its receipt names the pinned octos revision) | `serve --stdio --data-dir `; without a binary there is no kernel (no `PATH` or working-directory lookup). | -| 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 | +| Platform | Kernel | +| --- | --- | +| Desktop (macOS; Windows and Linux untested) | A child, `serve --stdio`: the shell's `Options::program`, else `$OCTOS_APP_CORE_BIN`, else the packaged `octos-kernel` beside the shell if its receipt names the pinned octos revision. The shell never searches `PATH`. | +| Android | A child: the APK's `liboctos.so serve --stdio`, built by [`tools/kernel-artifact.py`](../tools/kernel-artifact.py) | +| OpenHarmony | In process: `octos_cli::embedded::serve_io`, because a HAP may not exec | +| iOS | None | 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). +- **On demand.** The first `connect()` starts the kernel, and later consumers join that generation. A new generation waits until the old kernel has released its data directory. +- **Restart.** After a provider change, the `llm` host service calls `restart()`. Connections end with `CloseReason::Restarted`, and consumers reconnect. +- **Idle stop.** With Talk to Octos off, the kernel stops when its last connection closes. Each live broker holds one, so the kernel stays up while any app agent is prepared. +- **Exit and crash.** The kernel reads EOF on stdin when the shell exits or crashes. A kernel crash ends every connection with `CloseReason::Exited`, and the next `connect()` starts a new kernel ([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: +Native apps are reviewed, 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 entries from it, and CI runs it with `--check`. -| App | Hosting on macOS / Windows | Linux | Android, iOS, OpenHarmony | Desktop / phone | Assistant | +| App | macOS, Windows | Linux | Phones | Desktop / phone build (`shells`) | Agent | | --- | --- | --- | --- | --- | --- | -| 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 own agent, with its read tools only (`own_tools`); typing a command is the system agent's `terminal.run` (`confirm: host`, `auto_approvable: false`) | -| Calculator, Clock, Notes, Reminders, Weather | in process | in process | in process | default / default | the four `octos.*` services; their read tools also go to the system agent (`system_tools`) | -| Sheets, Reference | in process | in process | in process | opt-in / `mobile-apps` | – | -| AppCard | in process | in process | in process | opt-in / opt-in | its own kernel connection | - -So **only the Terminal is a process app**, and only on the desktop; `tools/native_apps.py` refuses plain `process` on Linux (`process-if-vulkan` only), so Linux without Vulkan and Wayland runs every app in process. App Hub stays in process because it hosts the Card runner every script app runs in. Rinx stays in process: ADR 0004 §2 moves it, by a reviewed change to its `hosting`, once the peer link and the process sandboxes exist. The peer link's shell side and the macOS and Linux sandboxes are on main now, but Rinx at its pin reaches its agent through the injected service ([section 3](#an-app-and-its-own-agent)), not the peer link, and its `hosting` is still `module` everywhere. +| App Hub (store, Card runner) | module | module | module | default / default | – | +| Rinx | module | module | module | default / default | injected service | +| Terminal | **process** | **process** with Vulkan and Wayland, else module | module | default / off | peer link; `terminal.run` is the system agent's | +| Calculator, Clock, Notes, Reminders, Weather | module | module | module | default / default | peer link | +| Sheets, Reference | module | module | module | opt-in / `mobile-apps` | – | +| Task (no module) | **process** | **process** | – | off / off; the desktop catalog starts it | – | +| AppCard | module | module | module | opt-in / opt-in | its own kernel connection | -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 build it from, or a sibling binary); without one it falls back to in-process (`manifest_default`). Desktop release packages are not on main yet ([#257](https://github.com/OctoSense-org/OctoSense/pull/257), in progress, which replaces the closed #94). A per-app override (`~/.makepad/wm/apps.splash`, `--module `) switches an app between module and process. +`AppRegistry::hosting` (`crates/shell/src/apps.rs`) decides at each launch. Builds without processes (native mobile, wasm) make every linked app a module, as App Hub and Settings always are. Elsewhere a linked native app follows its entry (`manifest_default`), unless switched in `wm/apps.splash` under the OctoSense home or by `--module `. It becomes a process only where it can be built from a checkout or found beside the shell. Task has no module, so it runs as a process even on Linux without Vulkan. -**Process hosting** is Makepad's window-manager hosting (`crates/shell/src/clients.rs`, `hub.rs`): +Release packages ship only `octosense` and the kernel, so there the Terminal runs in process: its own agent has only its read tools, the system agent gets no `terminal.run`, and Task is absent. **Not yet:** process apps in release packages. Rinx stays a module; ADR 0004 §2 lets a reviewed change to its `hosting` move it. -- The shell starts the app as a child with `--stdin-loop`, in its own process group, with `STUDIO_HOST=http://127.0.0.1:` and its client id. In a checkout it first builds the app's binary from the OctoSense workspace, a release build held to its `Cargo.lock` and run outside any sandbox, then starts that binary itself under the app's sandbox, never through `cargo run` (`clients::launch_plan`, [#225](https://github.com/OctoSense-org/OctoSense/pull/225)); an installed shell starts the sibling binary. -- The child connects back to the shell's **hub**, an HTTP/WebSocket server the shell binds on the first free loopback port in 8765–8784 (`WmHub::start`), and speaks Makepad's studio protocol (`AppToStudio` / `StudioToApp`). The hub admits a socket only with the per-launch secret the shell wrote to that child's stdin, once per launch, and refuses browser origins ([ADR 0004 §5](adr/0004-native-apps-hosting-and-peers.md#5-the-peer-link-for-process-hosted-native-apps)). -- 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: an unexpected death of a window the person has keeps its tile, closed with a Restart, and posts "App stopped" ([#130](https://github.com/OctoSense-org/OctoSense/pull/130); `ClientSlot::stops_in_place` in `clients.rs`). A warm instance, the AI pane, a preview or a client being closed goes away as before. +**Process hosting** (`crates/shell/src/clients.rs`, `hub.rs`). In a checkout, the shell builds the app from the workspace, held to its `Cargo.lock` and outside any sandbox. It then starts the binary itself, under the app's sandbox, with `--stdin-loop`; an installed shell starts the binary beside its own. The child joins the shell's hub, a loopback WebSocket on the first free port in 8765–8784, with a one-time secret it reads from its stdin. Catalog apps outside `native-apps.json` (Browser, Files and other upstream Makepad apps) run without a sandbox. A process app that dies leaves its tile closed with a Restart (`ClientSlot::stops_in_place`). -**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.** +**In-process hosting** (`crates/shell/src/module_host.rs`) gives each module instance its own Splash isolate and storage namespace, but its Rust code shares the shell's memory. Every call into a module runs under `catch_unwind` (`contain`). A panic fails the instance, answers its in-flight tool calls "outcome unknown" unless they only read, and shows a Restart. A panic during unwinding, `panic = "abort"` and FFI stay out of reach. ### Script apps -Script apps (the system apps News, Photos, Maps, Mail, Calendar, AI providers and YouTube, plus Camera on phones, 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.** +System and store apps run in App Hub's Card runner (`CARD_MODULE`), one isolate per instance with a file jail and a quota, and reach the shell only through `host.request` for granted families. `desktop/system-apps.json` and `phone/system-apps.json` list each shell's system apps. ```mermaid flowchart TB subgraph desktop["Desktop (macOS)"] - ds["OctoSense process
shell + App Hub + Card runner + Rinx"] + ds["OctoSense process
shell + App Hub + Card runner + Rinx + Notes, Clock, …"] dk["octos child
octos-kernel serve --stdio"] - dt["Terminal child
--stdin-loop"] - ds -- "stdin/stdout" --> dk - dt -- "hub WebSocket, IOSurface frames" --> ds + dt["Terminal and Task children
--stdin-loop"] + ds -- "stdin/stdout: OUP" --> dk + dt -- "hub WebSocket: frames, AI bus, peer link" --> ds end subgraph android["Android (Home)"] - as["Home process
shell + App Hub + Card runner + Rinx (all in process)"] + as["Home process
shell + App Hub + Card runner + Rinx + Notes, Clock, … (all in process)"] ak["liboctos.so child
serve --stdio"] - as -- "stdin/stdout" --> ak + as -- "stdin/stdout: OUP" --> ak end subgraph ohos["OpenHarmony"] os["Home process
shell + embedded octos task"] @@ -170,142 +144,163 @@ flowchart TB ## 2. Agents -**One kernel per shell; agents are sessions, not processes.** Inside the one kernel, agent turns run as Tokio tasks, while peers and sessions are durable state. A peer can have multiple context sessions and a turn can create several tasks; there is no one-agent/one-task mapping. See the [thread and task table](architecture-walkthrough.md#10-map-the-architecture-to-rust-execution). Tools such as file edits run in the kernel; octos's command tools would run as short child processes in the session's workspace, but OctoSense offers them to no agent ([section 4](#4-tools-and-grants)). +Every agent is a session in the one kernel, not a process. The README introduces [both kinds](../README.md#the-system-agent-and-the-app-agents) and [maps them to threads](../README.md#why-it-stays-light-on-memory-and-cpu). -| 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 in the shell's **system chat** (`crates/shell/src/system_chat/`, [#132](https://github.com/OctoSense-org/OctoSense/pull/132): Setup → Assistant → Assistant chat, F8, the dock's Assistant icon or the phone home's Assistant tile; on a desktop a medium pane at the left that the person can move and resize, full screen on a phone), or through a paired Talk to Octos client | 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)). The person talks to it in the app's own UI, or in the shell's **"Ask " panel** (`crates/shell/src/app_chat/`: the bar's "Ask ", Shift+F8; beside the system chat on a desktop, a full-screen sheet on a phone), which shows both lanes with their speakers and a composer: Send while the person's lane is idle (even while the system agent works), Stop for the person's own turn, and "Stop the system agent's task" on the system agent's running row | On main for the native apps with an agent (Rinx, the Terminal, Calculator, Clock, Notes, Reminders, Weather) and every script app with an agent (today the system apps News, Mail, Photos, Maps, YouTube, Calendar on the desktop and Camera on phones), after first-use consent; see below | +### The system agent + +The system agent is the session `_main:api:octosense#system` (`SYSTEM_SESSION` in `crates/kernel/src/network.rs`). The person talks to it in the system chat (`crates/shell/src/system_chat/`), which runs on its own thread and connects only while its pane is open or a turn runs. + +Its kernel tools are exactly `SYSTEM_AGENT_TOOLS` (`crates/kernel/src/system_tools.rs`): the four `peer_*` tools, files in its own workspace, `ask_user_question` and media viewing, memory, `web_search`, `web_fetch` and `tool_search`. It never gets `peer_handoff`, `peer_close` or octos's shell ([enforcement](#the-system-agents-tool-set)). -Each app peer has, separately from every other: +The system chat registers host tools on the session (`ShellSystemHost::declarations` in `system_chat/session.rs`): -- a **workspace**: the folder the peer's session is bound to. A peer created since the octos 665209e5 pin gets its account folder (`apps//accounts//`, ADR 0004 §11) as `peer/prepare`'s `cwd` (`host_tools::agent_workspace`), recorded beside its host token so a resume names the same folder; a peer created before keeps the kernel-provisioned one, since octos refuses to resume a peer under another workspace (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**: exactly the kernel tools its manifest grants (`generic_tools`, never octos's `shell`) plus the tools the shell registers (octos [#2567](https://github.com/octos-org/octos/pull/2567), UPCR-2026-035): after every `peer/prepare` and reconnect the broker registers, on the connection that drives the peer's turns, the app's declared tools and the other apps' shareable tools granted to it, each marked with its owning app (`crates/shell/src/host_tools/`), with `generic_tools` set to exactly the kernel tools the app's manifest declares and the person granted (a native app's `native-apps.json` `agent.generic_tools`, a script app's plain names in its manifest's `agent.tools`; an app that names none keeps none). The Terminal declares `terminal.run`, `terminal.read_screen` and `terminal.read_scrollback`. The system apps declare theirs in `apps//bundle/tools.json`, each run on a host service: News `news.list`, `news.read` and `news.notify` (the `news` service), Mail `mail.notify` (the `mail` service), Calendar `calendar.events`, `calendar.add_event`, `calendar.remove_event`, `calendar.notify` and `calendar.agenda` (the `calendar` service), and Photos, Maps, YouTube and Camera `.notify` (the shell's notice service, `crates/shell/src/glance_notice.rs`, since they have no service of their own). A peer that cannot register runs no turn; -- **request contexts** (`peer/context/open`): one per client instance (a Rinx mini app), each with its own transcript, folder `contexts//` inside the peer's workspace and child memory namespace. Such a client's context cannot read the files beside it directly; it reads its account's data through the shell's host read tools `files.list`, `files.read` and `files.search` (ADR 0004 §11, `crates/shell/src/host_tools/files.rs`), which never show another context's folder. Besides those per-client contexts (`OctosAppService::open_context`: Rinx's mini apps, a process app's `octos.session.open` with a `client`), each app conversation is one more request context: the person's own conversation with an app agent, from the shell's "Ask " panel, the app's UI or a card's in-card chat, runs in the person's lane, a context opened with `share_history` that runs in parallel with the peer's own session (the system agent's lane) and shows each lane the other's recent turns (ADR 0004 §6, decided 2026-09-29; below). That conversation context is opened with octos's `read_parent` where the manifest says the agent works in the account folder (`storage.agent_workspace: "account"`, octos#2647): it reads the account folder read-only, never another context's folder, and writes only its own. +- `agents.list` and `agents.ask` (declared in `crates/shell/src/agents.rs`): always. +- `agents.provision` and `agents.status` (also in `agents.rs`): for Mail only, in builds with App Hub or native mobile ([Mail events](mail-agent-events.md)). +- `terminal.run`: while Setup's Command execution is on and the Terminal runs as a sandboxed process (`system_chat/grants.rs`, `terminal_target`). +- Each native app's `agent.system_tools`: when this build links the app or can start it as a process. A closed app answers "Open <App> first". -Who gets a peer today (`crates/ai-host/src/lib.rs`, `Policy::shipped()`; `crates/app-peers/src/hosted.rs`, `effective_services` = declared ∩ supported ∩ policy): +octos lists only prepared peers, so the shell tells the system agent the rest: a note with its turn when the apps' agents change, and `agents.list` with each app's agent, state and peer slug. `agents.ask` shows the first-use sheet and holds the call until the person answers and, for a script app, the peer is ready. The system agent never approves anything; `peer_respond` answers only questions. -- **Native apps** whose `native-apps.json` entry grants `agent.octos` (the four `octos.*` services, generated into `crates/ai-host/src/native_agents.rs`): Rinx, the Terminal, Calculator, Clock, Notes, Reminders and Weather, after first-use consent. A native app's peer is its open instance's, so it exists while the app is open (`agents::prepare` skips native apps). -- **Script apps with an agent**: a manifest that declares `octos.*` or an `agent` block, or a bundle that ships `tools.json` (`crates/shell/src/apps.rs`, `agent_apps`). Today that is every system app except AI providers: News, Mail, Photos, Maps, YouTube, Calendar on the desktop and Camera on phones, each with an `agent` block and `tools.json` and none declaring `octos.*`. One peer per app/account with broker identity `card.`, using `device` for accountless apps or the host-reported active account (Mail requires sign-in) (`crates/ai-host/src/contained.rs`, [#106](https://github.com/OctoSense-org/OctoSense/pull/106)), once the person allowed it at first use (the shipped gate; `OCTOSENSE_CONTAINED_APPS=1` asks nobody, `0` turns it off). The shell prepares it as soon as it is allowed, and at startup (`crates/shell/src/agents.rs`, `contained::prepare`), so the system agent's `peer_list` shows it even when the app never calls `octos`; the app's own `host.request("octos.*")` stays limited to what its manifest declares. -- **AppCard** (opt-in) takes its own kernel connection and sessions, not a peer. -- **Process apps** whose `native-apps.json` entry grants `agent.octos` services reach their peer through the **peer link** (`crates/shell/src/peer_link/`, [#130](https://github.com/OctoSense-org/OctoSense/pull/130)), after first-use consent. The Terminal does: it opens its agent's link with Makepad's `OctosPeer` and answers its read tools there. +### App agents -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). +An app agent is a host-owned octos peer for one (app, account), owned by the system agent's session (octos UPCR-2026-034) and driven by a broker (`crates/app-peers/src/broker.rs`). Its memory namespace is `app//acct-`, and the broker refuses a kernel that does not confirm it. Its workspace is the account's folder ([section 6](#6-storage-and-secrets)), and its host token sits in a record under `/../app-peers`, owner-only on Unix. The broker registers its tools after every `peer/prepare` and reconnect; a peer whose registration fails runs no turn. + +A script app's peer is `card.` (`crates/ai-host/src/contained.rs`). Apps without accounts act for `device`; Mail acts for the account signed in last. `apps::agent_apps` decides which apps have an agent, and an app granted nothing gets no broker. + +- A script app's peer is prepared once the agent is allowed and at each startup (`agents::start`), so `peer_list` shows it while the app is closed. +- A native app's peer belongs to its open instance. With several instances, the oldest drives it and the next takes over (`driver_of`, `take_over`). +- Signing out suspends the account: its contexts close, its tool calls get `signed_out`, and no `peer/input` turn starts. Signing in resumes the same peer. + +### Two lanes + +The system agent's lane is the peer's own session. The person's lane is a request context opened with `share_history` (`open_conversation`), new for each handle, where the person's turns (origin `person`) and the app's own (origin `app`) run ([One app agent, two lanes](../README.md#one-app-agent-two-lanes)). Each turn starts with a read-only view of the other lane's recent text: by default the last 20 rows within 16 KiB, plus any turn still running. + +The trigger decides how approvals treat a turn. The Ask panel sends `ContextOp::TurnFrom { trigger: TurnTrigger::Person }`. A bare `ContextOp::Turn` counts as `Unknown`, the least trusted, and a script's `trigger: "person"` becomes `AppSaysPerson`, which the relay treats as the app's own run (`trigger_of`). Plain request contexts (`open_context`, used for Rinx's mini apps) share no history and are fenced to `contexts//`. + +### The "Ask <app>" panel + +The shell draws the person's lane for every app with an agent (`crates/shell/src/app_chat/`), in the system chat's pane: beside the system chat on the desktop, full screen on a phone. + +- **Open** needs the agent allowed (the first-use sheet asks first) and, for a native app, the app open, because its peer belongs to its window. +- **Send** starts a person turn, even while the system agent's lane runs, or answers the app agent's open question. +- **Stop** ends only the person's turn; "Stop the system agent's task" ends the other lane's. Stop on the approval sheet or question card, or `octos.turn.interrupt` on an app's conversation, ends both. +- **Questions** from the app's conversation appear in the panel while it is open, otherwise on the shell's question card. +- **Close** hides the panel but keeps its context until the panel opens for another app, the agent is turned off, or the peer goes away. ### 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). +All agents live in the one kernel, so a crash stops them all mid-turn. The shell and its apps keep running, and apps see their assistant as unavailable. Tool calls on the dead link end without running. Sessions, blackboards, memory and peer bindings are on disk, so after the next `connect()` the brokers resume their peers with the stored host tokens. ## 3. Communication ```mermaid flowchart LR subgraph shell["Shell (host connection)"] - broker["app-peers broker"] - relay["host-tool relay
(crates/shell/src/host_tools)"] + broker["app-peers broker
one per app instance"] + relay["host-tool relay
crates/shell/src/host_tools"] + router["approval router"] end subgraph kernel["octos kernel"] sys["system agent"] - p1["Calendar peer"] - p2["Mail peer"] + p1["Calendar's peer"] + p2["Mail's peer"] bb[("blackboard
peers/<slug>/result.md, turns.txt")] end - rinx["Rinx (module)"] -- "OctosAppService" --> broker + rinx["Rinx (module)"] -- "injected OctosAppService" --> broker + mods["Calculator, Notes, … (modules)"] -- "peer link, in memory" --> broker + proc["Terminal (process)"] -- "peer link, on the hub socket" --> broker card["script app"] -- "host.request(octos.*)" --> broker - proc["process app"] -- "peer link (the Terminal)" --> broker broker -- "OUP: peer/prepare, peer/context/open, turn/start" --> kernel sys -- "peer_send_input" --> p1 + kernel -- "peer/input, peer/tool/call, approvals" --> broker + broker -- "tool calls, approvals" --> relay + relay --> router p1 -- "writes" --> bb - sys -- "peer_gather / peer_list" --> bb - kernel -- "peer/input, peer/tool/call, host_tool approvals" --> relay + sys -- "peer_gather, peer_list" --> bb ``` ### 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.** +OUP is JSON-RPC 2.0, `octos-ui/v1alpha1` (octos [`api/OCTOS_UI_PROTOCOL_V1_SPEC_2026-04-24.md`](https://github.com/octos-org/octos/blob/056173e85b150e387805fc307fe231064ac1ed35/api/OCTOS_UI_PROTOCOL_V1_SPEC_2026-04-24.md)). By default it runs one frame per line over the child's stdin and stdout, and every consumer in the shell shares that stream. `crates/kernel/src/router.rs` gives each request a kernel-unique id, returns each reply to its own consumer, and sends each notification to the consumers that named its session. -- **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: +**Talk to Octos** ([ADR 0003](adr/0003-shared-octos-client-access.md); octos [`docs/HOST_MANAGED_SERVE.md`](https://github.com/octos-org/octos/blob/056173e85b150e387805fc307fe231064ac1ed35/docs/HOST_MANAGED_SERVE.md)) restarts the kernel, on the desktop and Android, as `serve --host 127.0.0.1 --host-managed`; on Unix the shell passes it a listener bound once. Two tokens reach the kernel on its stdin, never in its 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 | +The host token stays in the shell and reaches everything. The external token goes to a paired web client (by one-time code) or to a terminal client of the same user (`client-connection.json`, owner-only on Unix). It opens only `/api/ui-protocol/ws`, as `_main`, for the system conversation and the client's own turns: no `peer/*` method, no app peer, and only octos's fixed `EXTERNAL_TURN_TOOLS`. - 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 and app agents inside the kernel -### System agent and app agents, inside the kernel +octos's peer tools ([`crates/octos-agent/src/tools/`](https://github.com/octos-org/octos/tree/056173e85b150e387805fc307fe231064ac1ed35/crates/octos-agent/src/tools)) work at depth 1: a peer cannot create, steer or close another peer. -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/`. +- **To an app agent:** `peer_send_input`, at most 64 KiB, from the peer's originator only. octos hands that turn to the shell ([below](#how-the-shell-runs-the-system-agents-input)). +- **Back:** the blackboard, the only channel between peers. Each peer turn writes `peers//result.md` and a line in `turns.txt`, which the system agent reads with `peer_gather` and `peer_list`. +- **Questions:** the broker hands each `ask_user_question` to `crates/shell/src/questions/`, which routes it by its turn's origin ([Questions](../README.md#questions)). -- **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). -- **Which apps' agents the system agent can reach.** `peer_list` lists the prepared peers: the shell prepares every allowed app's agent. For the ones it cannot list (not yet allowed, or off) the shell tells it: a note with the system chat's turn whenever the apps' agents changed, and the host tools `agents.list` and `agents.ask` on the system session (`crates/shell/src/agents.rs`). `agents.ask` shows the first-use sheet, which only the person answers, and waits for them: it is declared `risk: act`, `outward: true`, `confirm: "app"`, so the kernel holds the call as it holds an approval, and the system chat answers it once the person has answered and the app's peer is ready, with the peer's slug, so the system agent goes on with the request in the same turn (after 10 min without an answer it says the person has not answered yet, and the sheet stays up; [#274](https://github.com/OctoSense-org/OctoSense/pull/274)). -- **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). octos keeps `ask_user_question` on host-driven turns (`generic_tools` is an exact list the host sets, and omitted keeps the whole roster). ADR 0004 §6 (decided 2026-09-29) has the shell route each question to the right conversation: the app's own conversation, or the system chat for system-agent turns, `peer/input` turns included; there is no `host.ask`. **On main**: the system chat answers the system agent's own questions on its session; the broker hands an app peer's `user_question/requested` (its own session, a request context, a `peer/input` turn) to the shell's request model (`crates/shell/src/questions/`) with the turn's origin (reported by octos once it carries it, else derived: the turns the broker started for `peer/input` are the system agent's), and the model routes it by that origin: a person's or the app's turn to the app's conversation (its "Ask " panel while that is open, otherwise the question card in the shell's approvals overlay, `crates/shell/src/approvals/view.rs`: one surface at a time, [#279](https://github.com/OctoSense-org/OctoSense/pull/279)), a system-agent turn to the system chat. Only the person answers, on a shell surface (`user_question/respond` on the broker's link); the app's context hears `user_question/handled_by_host`, and the broker refuses an app's attempt to answer an id the host holds. A question closes when its turn ends. -- **Two parallel lanes per app agent, sharing history read-only.** ADR 0004 §6 (decided 2026-09-29, replacing the same day's single queued conversation of [#166](https://github.com/OctoSense-org/OctoSense/pull/166) and octos [#2626](https://github.com/octos-org/octos/pull/2626)): the system agent talks to the peer's own session (`peer/input`), and the person (the app's UI, its cards, a process app without a `client`) in a request context the broker opens with `share_history` (octos [#2636](https://github.com/octos-org/octos/pull/2636), UPCR-2026-034), a new context id per handle (its history also holds the person's rows of the earlier handles of the same account and instance). The two run in parallel: a person's message waits only for its own context's previous turn, and `peer/input` keeps its own queue on the peer's session. At each turn's start the kernel shows the model the other lane's last user/assistant text rows (20 by default, at most 50, within 16 KiB; tool rows dropped) as a read-only block that is never written into the reader's transcript; there is no shared transcript. Every turn keeps its `origin` (`person`, `system_agent`, `app`; the kernel's `[from …]` marker; the host cannot relabel a system-agent turn), and a person's turn leaves a round on the blackboard (`origin: person`, `context: `) that the system agent reads with `peer_gather` and that fires no fleet synthesis on its own. `OctosAppService::open_conversation` is the person's lane; `open_context(client)` stays a plain request context (Rinx's mini apps). The app's follower (`OctosContext::subscribe`; `conversation` frames on the peer link) gets both lanes' events, each with `lane` and `speaker`; `octos.session.history` is both transcripts merged by `persisted_at`, each row with `lane` and, for a marked user message, `speaker` and `display_text`. Accepted hazards: file edits are not locked across lanes (each lane edits its own folder, a context's is `contexts/`), the unknown-outcome marks are shared, and parallel turns can overshoot the peer's token budget slightly. +### How the shell runs the system agent's input -### The host-owned path: `peer/input` +octos delivers the system agent's input to the connection that drives the peer, as `peer/input {peer, session_id, input_id, turn_id, text}`. The broker starts the turn itself with the kernel's turn id, so the turn has the app's tools, memory and approvals ([How the system agent and an app agent talk](../README.md#how-the-system-agent-and-an-app-agent-talk)). -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. **On main**: octos sends it since the 665209e5 pin (octos [#2567](https://github.com/octos-org/octos/pull/2567)); the broker (`crates/app-peers/src/broker.rs`) starts the turn on its own link with the kernel's `turn_id`, drops a repeated `input_id`, and starts nothing for a signed-out account or an app the person has not allowed (`host_tools::ShellToolHost::admit_input`), refusing it with `peer/input/reject` and the reason (octos [#2621](https://github.com/octos-org/octos/pull/2621)); a turn that fails to start is refused the same way. With several instances of one app on the kernel, the oldest live one for the account drives the peer (registers its tools, takes its `peer/input`), and the next takes over when it closes ([#201](https://github.com/OctoSense-org/OctoSense/pull/201)). The kernel admits one turn per session and queues none (`turn_in_progress`), so the broker keeps one queue per peer for the system agent's inputs on the peer's session, one turn at a time (a start refused `turn_in_progress` is retried with the same turn id); when 8 wait, an input is refused `busy`. The person's messages run in their own lane and never wait in it. Nothing holds a lane for long: an approval or question on the peer's session or a context expires after 10 min (`OCTOSENSE_PROMPT_DEADLINE_SECS`; the approval router denies it, the request model declines the question with free text, never approving, and both show "Expired: no answer in 10 min" until dismissed), and a turn still running 30 s later is interrupted by the broker so the next queued turn starts. The person's Stop ends the running turns of both lanes, the person's and the system agent's: `octos.turn.interrupt` on a conversation (apps, cards, the peer link) and "Stop 's agent" on the shell's approval sheet and question card (`approvals::stop_agent`, `broker::interrupt_where`). The "Ask " panel stops one lane at a time (`broker::interrupt_lane_where`): its Stop ends the person's own turn, and "Stop the system agent's task" the system agent's. The shell's kernel consumers share one kernel connection (`crates/kernel/src/router.rs`), so every broker and the system chat are the same host connection to octos; each answers only the calls of its own peer or session. +The broker answers `peer/input/reject`, with a reason, when the agent is not allowed, the account is signed out, the workspace was refused, or the turn fails to start (`ShellToolHost::admit_input`). The kernel runs one turn per session and queues none, so the broker queues the system agent's inputs per peer and starts them one at a time. With 8 waiting, the next is refused `busy`. The person's lane never waits in that queue. ### An app and its own agent -| Hosting | Channel | Status | +An app never speaks OUP and never sees the host token, and the shell stamps the app's identity on every call ([How an app uses its agent](../README.md#how-an-app-uses-its-agent)). There are three paths: + +| Path | Used by | How it works | | --- | --- | --- | -| In-process native module, through the peer link | Makepad's `OctosPeer::open` parks a channel pair; the module host claims it for the instance whose code opened it and the shell serves it with the process apps' **peer link** (`peer_link::module_connected`, `octosense_ai_host::module_peer`), so the app does not know how it is hosted | On main ([#142](https://github.com/OctoSense-org/OctoSense/issues/142)); used by Calculator, Clock, Notes, Reminders and Weather, and by the Terminal where it runs in process | -| 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_gate` (`ContainedGate::Consent` by default) and first-use consent; tool approvals its peer raises go to the shell's router. None of the system apps calls `octos.*` or draws a chat: the person talks to their agents in the shell's "Ask " panel, on the same peer, and the shell drives them for the system agent | 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` | Shell side on main ([#130](https://github.com/OctoSense-org/OctoSense/pull/130), `crates/shell/src/peer_link/`, frames routed from the hub connection in `lib.rs`); used by the Terminal | +| Peer link (`crates/shell/src/peer_link/`) | Calculator, Clock, Notes, Reminders, Weather, Terminal | Makepad's `OctosPeer` client. A process app's link rides its hub socket. A module's `OctosPeer::open` parks an in-memory channel, which `module_host` claims for that instance and the shell serves as the same frames (`peer_link::module_connected`). `serve_tools` answers the agent's tool calls. | +| Injected service | Rinx | `ai_host::offer` before the module's `create`, and `injection::claim` inside it, give the instance a scoped `OctosAppService` (`open_conversation`, `open_context`). | +| `host.request("octos.*")` | store apps | The Card runner's `octos` host service (`crates/ai-host/src/contained.rs`): only the services the manifest declares, after first-use consent. | -### Makepad's AI services bus vs OctoSense's app agents +On the peer link, the socket or module instance is the identity, and a call's account, context and client come from the shell's records of the contexts the app opened. When a process dies, its outstanding calls fail (`outcome_unknown` unless they only read), and the peer stays. -Two models live side by side (ADR 0004, "Two AI models"): +Rinx, at its pinned tag, uses the injected service only for its mini apps' contexts. The person reaches its agent in the "Ask Rinx" panel; Rinx's own assistant tools are on the AI services bus, and its agent declares no tools. Script apps get no pushed events: `octos.turn.start` returns the collected reply, and `octos.session.history` returns both lanes merged by time (**Not yet:** streaming). No system app declares `octos.*`; the shell drives their agents. -| | 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`; a native app's own tools when the app holds no peer link (`host_tools/relay.rs`, `serves_on_bus`). 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 | +### The AI services bus -**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 how the system agent talks to app agents; the shell's relay uses it only to run a native app's tool in the app's open instance (the Terminal's host-confirmed `run`, and an app's own tools when it holds no peer link). 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. +The bus is Makepad's other AI model: one central conversation, the desktop's AI pane (`aichat`), calls typed tools that apps register with a risk level. The shell's half, `crates/shell/src/ai_bus.rs`, stamps each app's frames with its endpoint, forwards registrations to the pane, routes the pane's calls, answers the `os` service itself, and holds `confirm: host` calls for the approval router. OctoSense uses it for the pane, for Rinx's assistant tools, and as the relay's route to a native app's tools when the app has no executor or peer link; `terminal.run` always takes it. App agents do not use the bus: it carries no account, context or caller. ## 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: +The manifest declares, the person grants at install, and the shell enforces on every call (ADR 0004 §12). A script app declares its tools in `tools.json`; a native app declares its agent in its `native-apps.json` entry (`agent.octos`, `tools`, `own_tools`, `system_tools`, `grants`, `generic_tools`, `budget`, `tool_policy`). The README's [What an app gives its agent](../README.md#what-an-app-gives-its-agent) explains most of them. `tool_policy` sets a tool's confirmation: the Terminal's `run` is `confirm: host` with `auto_approvable: false`, so no standing rule ever answers it. -| Source | Example | Runs | Status | +| Source | Declared in | Runs on | Today | | --- | --- | --- | --- | -| The app's own tools (`tools.json`: name `.`, schemas, `risk`, `confirm: host` or `app`, `shareable`) | `news.list`, `terminal.read_screen`, `mail.notify`, `calendar.add_event` | the app's host service, module or process (or the shell's notice service), called by the shell | On main (`crates/shell/src/host_tools/`): declared from `native-apps.json` `agent.tools` (native) or the admitted bundle's `tools.json` (script, `host_tools/script_apps.rs`, with App Hub's loader), authorized by (owning app, tool) and caller, arguments and results checked against the declared schemas, budgeted, stamped with account, context and client, and routed to a process app's peer link, an in-process module's executor (`OctosAppService::set_tool_executor`), a script app's host service (`HostServiceExecutor`: the service of the tool's namespace, which the app's manifest must be granted unless it is a system app's own, as `calendar` is Calendar's) or the Terminal's AI bus service. News offers `news.list`, `news.read` and `news.notify`; Mail its account-scoped reads, draft/proposal tools, `mail.notify` and `mail.publish_card` (see `apps/mail/bundle/tools.json`); Calendar `calendar.events`, `calendar.add_event`, `calendar.remove_event` (destructive, `confirm: host`), `calendar.notify` and `calendar.agenda`; Photos, Maps, YouTube and Camera `.notify`, which the shell's notice service answers; the Terminal `terminal.run` and its read tools | -| The system toolbox, by capability (`research`, `crawl`) | `toolbox.search`, `toolbox.web_read`, `toolbox.deep_crawl`, `workflow.run` | host (`crates/toolbox`) | On main behind the feature `toolbox-peers` ([#151](https://github.com/OctoSense-org/OctoSense/pull/151); in the phone build by default, off on the desktop; `crates/ai-host/src/toolbox_peers.rs`): `research` gives `workflow.run`, `workflow.fork`, `toolbox.search` and `toolbox.web_read`, `crawl` gives `toolbox.deep_crawl`, offered only after consent. Until App Hub checks these capabilities, a script app's declaration counts only for a system app, and no app declares either yet | -| octos's generic tools | file reads fenced to the workspace, memory, `web_search`, `deep_search` | octos | On main per app: each peer is registered with exactly its granted `generic_tools` (Rinx: its workspace files, `ask_user_question`, memory, the web); an app granted none keeps none; octos's `shell` is never among them (the generator, the catalog and the script loader drop it), and the `_main` profile's `tool_policy` denies it too | -| Other apps' shareable tools, routed by the shell | Calendar's agent calling Mail's `mail.send` (planned: Mail declares no such tool yet) | the owning app, via the shell | On main (`relay::Catalog`): granted by (owning app, tool), from `native-apps.json` `agent.grants` (native) or the dotted names in a script app's `agent.tools` (at install), registered marked with the owner, checked on every call. News's `news.list` and `news.read` are shareable, but no app asks for another app's tool yet | -| Command execution | `terminal.run` (the Terminal's shareable tool: `confirm: host`, `auto_approvable: false`) | host tool, in a terminal the person sees | On main for the system agent: registered on its session while Setup › Assistant › Command execution is on and the Terminal runs as its own sandboxed process (its newest launch ran inside its OS sandbox: macOS, and Linux with Vulkan and Wayland; not Windows, whose sandbox is not built), each call approved live through the router as a command, typed into the running Terminal. The in-process Terminal offers its reads only and, by ADR 0004 §10 (decided 2026-09-29), stays read-only. Granting it to app agents is step 11 | +| The app's own tools, `.` | `tools.json`; `agent.tools` | the app's host service, the notice service, or the app's window | `own_tools` narrows the Terminal's to its two read tools; Mail's agent drafts and proposes replies but has no tool that sends | +| octos's kernel tools | plain names in `agent.tools`; `agent.generic_tools` | octos | script apps only `ask_user_question`; Rinx files, memory and web; other native agents none | +| `files.list`, `files.read`, `files.search` | the shell, on consented peers with a workspace (Unix) | the shell, over the caller's account folder | at most 128 KiB a read, 500 entries a listing, 100 matches a search | +| Other apps' shareable tools | dotted names in `agent.tools`; `agent.grants` | the owning app, through the relay | News shares `news.list` and `news.read`; no app asks for one yet | +| The toolbox (`toolbox.*`, `workflow.*`) | the `research` and `crawl` capabilities | the shell ([`crates/toolbox`](../crates/toolbox/README.md)), feature `toolbox-peers`, default on phones | no app declares either yet | +| `dev.run` | developer mode | the shell (`host_tools/dev_run.rs`) | covered apps only | -**What an agent puts on the glance screen.** The `.notify` tools fill the shell's fixed notice template (`crates/shell/resources/glance/notice.card`); Calendar's `notify` and `agenda` fill its event/agenda templates. The model supplies text for those tools. Mail additionally offers `mail.publish_card`, which accepts model-authored L0 source and can attach a host-issued draft binding. Publication uses the calling app's admitted `glance` grant; repeated card IDs replace that app's publication. +An app's `AGENT.md` and skills are not tools: the broker sends their text with every turn as host guidance (`crates/app-peers/src/guidance.rs`). -A phone notification opens its exact card workspace above the current screen; a stale card falls back to Glance. The compact Glance feed executes no generated UI. Tapping a summary expands the resident workspace to the root safe viewport without launching another app. Agent-enabled publishers get native Card / Chat even when their L0 or Splash source omits `sys.chat`; the host binds bounded publication and L0 state context to the original account. Mail's specialized Email / Chat reads one durable draft and uses separate host review for sending. Explicit `sys.chat` retains its declared thread; the explicit Mail demo still uses canned replies. Desktop toasts, the Glance panel and dismiss/undo keep their existing entry points. See [the card workspace walkthrough](mail-composable-cards.md#shared-workspaces-for-all-card-publishers) for code and device evidence. +### The relay -**The system agent's tool set** (`crates/kernel/src/system_tools.rs`, [#117](https://github.com/OctoSense-org/OctoSense/pull/117)). **On main**, enforced: +The relay (`crates/shell/src/host_tools/`) takes every `peer/tool/call` from the brokers and the system chat, on the UI thread: -- Its default list, `SYSTEM_AGENT_TOOLS`: supervision (`peer_send_input`, `peer_gather`, `peer_list`, `peer_respond`; not `peer_handoff`, and not `peer_close`, which the `_main` profile's `tool_policy` denies to every agent), 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); only command execution has a switch today. -- **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. -- **Exactly its list.** Every kernel start sets the system session's kernel tools to `SystemAgentTools::kernel_tools` with octos's durable, host-only `session/tool_list/set` (octos#2648), on the host's own connection before any consumer's frame; a grant change sets it again. Every turn on the session is narrowed to it, whoever starts it; host tools (`terminal.run`) are registered beside it and the `spawn` family is never on it. Real-kernel test: `a_system_agent_turn_is_offered_exactly_the_system_agent_tools`. -- **Command execution from Settings** (off by default, each command approved live): Setup › Assistant › Command execution sets the grant; while it is on, the system chat registers `terminal.run` on the system session over its own connection (`peer/tools/register` without `peer`), and withdraws it the moment it is turned off. The chat's connection is the host's own, so octos needs no credential for it (octos#2657, [#146](https://github.com/OctoSense-org/OctoSense/issues/146)): the tool is offered at once, before any app's agent has started. The grant takes effect only where the Terminal runs as its own process and its newest launch ran inside its OS sandbox (ADR 0004 §10 and §12, decided 2026-09-29; `system_chat::grants::terminal_target`): on Linux without Vulkan, on Windows (no sandbox yet) and on phones `terminal.run` is not registered, and Setup says it needs the Terminal as a sandboxed process. +1. **Authorize** by (owning app, tool) and caller: an app's own agent may call its own tools, another app's agent only the tools granted to it (`Catalog::may_call`), and the system agent only its host tools. Missing consent or a suspended account refuses the call. +2. **Check** the arguments (at most 64 KiB) against `input_schema`, and charge the caller's budget: 32 calls a turn and 1000 a day unless `agent.budget` says otherwise. +3. **Route** to the owning app's executor (`HostServiceExecutor`, or a module's `set_tool_executor`), else its peer link, else its AI bus service. A `confirm: host` tool also served on the bus skips the link. +4. **Confirm** a `confirm: app` call: acknowledge it to the kernel, then hand it to the owner's sheet ([section 5](#5-approvals)). +5. **Answer once,** checked against `output_schema` (at most 256 KiB); nothing runs after a cancel. Each call is audited, with a digest of its arguments, in `logs/tool-calls.jsonl`. -**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. +A script app's `implemented_by: "host-service"` tool runs on its namespace's host service, as the app, if the app was granted that family or owns it as a system app. The shell's `NoticeService` answers `.notify` for Photos, Maps, YouTube and Camera. **Not yet:** `implemented_by: "app"` has no executor and store apps have no host service, so a store app's agent cannot act through tools of its own. -## 5. Approvals +### The system agent's tool set + +Before every kernel start, `enforce` (`crates/kernel/src/system_tools.rs`) writes the `_main` profile's `tool_policy`, denying octos's shell (`group:runtime`) and `peer_close` to every session. It replaces only a policy OctoSense wrote, and if it fails, no kernel starts. Then each start sets the system session's tools to `SYSTEM_AGENT_TOOLS` with octos's durable, host-only `session/tool_list/set`, which narrows every turn on the session; host tools are registered beside it. App peers get exactly their granted `generic_tools`. + +### What an agent puts on the glance screen -**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 glance service (`crates/shell/src/glance.rs`) publishes every card as the calling app, under the account the host records, and only with the app's `glance` grant. An app may publish at most 6 times a minute and keep at most 4 cards; the glance screen shows 6 of the 32 cards the service keeps. `mail.publish_card` can also bind a card to one of Mail's saved drafts, and a bound card cannot move to another account, email or draft. + +On a phone, the glance feed draws compact summaries and runs no generated UI (`mobile_pages.rs`). Tapping a summary expands it into a resident full-screen workspace (`glance_sheet.rs`), and a notification opens its card's workspace directly; on the desktop the workspace opens centred. A publisher with an agent gets Card / Chat tabs even when its card declares no `sys.chat` (`WorkspaceChat` in `glance_card.rs`), and a Mail reply card gets Email / Chat over one saved draft. [Composed Mail cards](mail-composable-cards.md#shared-workspaces-for-all-card-publishers) has the details. The README describes the card templates under [How the system agent and an app agent talk](../README.md#how-the-system-agent-and-an-app-agent-talk), and the cards' own policy and in-card chat under [Cards and questions](../README.md#cards-and-questions). + +## 5. Approvals -**Agent tool calls and the person's actions.** The router governs `peer/tool/call`; ordinary app UI actions use the publishing app's policy through the Card runner's service gate and the host service's checks. Mail sending additionally requires a host-owned exact-message review and physical Android approval, including for its legacy composer; generated buttons, developer mode and standing rules cannot authorize SMTP. A look-alike card cannot answer `approval/respond` or obtain the Mail review capability. Each generated card runs in an isolate under its publisher's policy. L0 actions dispatch through `glance_card::LiveCards`; on the phone they execute inside the expanded workspace, while collapsed summaries stay inert. +A grant lets an agent have a tool; an approval lets this call, with these exact arguments, run. Reads and in-app actions run once granted, while destructive or outward calls need the person, live or by a standing rule. Only the person approves (ADR 0004 §8). The router covers agent tool calls only: what the person does in an app or its cards is the app's own action. -**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: +The approval router (`crates/shell/src/approvals/router.rs`) decides in this order: ```mermaid flowchart TB @@ -334,22 +329,32 @@ flowchart TB sheet --> person ``` -0. **External clients' turns** are not the shell's (octos#2624): the router holds and answers nothing (`Route::LeftToClient`) and posts a notice; the Talk to Octos client that started the turn answers it. -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`, Settings with a typed phrase on the desktop (Setup › Developer options) or on a phone Turn on in Home's Settings › About phone › Developer options, which seven taps on Build number reveal, for the apps chosen under Developer options › Apps it covers (kept per home in `assistant/developer-apps.json`); 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 registered on a covered app's peer as its own host tool and run by the shell (`crates/shell/src/host_tools/dev_run.rs`), never on the system agent's session. -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. An in-process app registers its sheet through its service (`OctosAppService::set_confirm_sheet`, bridged by `host_tools::SheetBridge`), a process app's is registered when its peer link opens; Rinx at its pinned tag does not yet, so its send sheet still answers inside Rinx. The one `confirm: app` tool on main is the shell's own `agents.ask` ([section 3](#system-agent-and-app-agents-inside-the-kernel)), which the system chat holds itself. -3. **`auto_approvable: false`**, **outcome unknown** and calls that did not come on the host's own connection 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". Contacts come from Mail's host service data (the person's own accounts and the addresses they sent mail to; `contacts.rs`), and only after the person turns on "Use my contacts in approval rules" in Settings (off by default); until then "recipients in contacts" never matches. OS address books are a follow-up. 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. +0. **An external client's turn** is left to that client; the router only posts a notice. +1. **Developer mode** approves everything for the apps it covers, `auto_approvable: false` and `confirm: app` included, on the host's own connection. +2. **`confirm: app`** goes to the owning app's sheet, which shows the caller. An app with no registered sheet gets 120 seconds, then a visible refusal. +3. **The person, always,** for `auto_approvable: false` tools such as Terminal commands, unknown outcomes, and calls not on the host's connection. +4. **Standing rules** on (owning app, tool), whoever calls. Runs started by incoming content or an unknown trigger skip them unless a rule opts in. +5. **A shell-drawn sheet** with the owning app, tool, exact arguments and any calling app. The system agent's approvals for one request can share one sheet. -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. +**Mail sending** never goes through the router. Mail's own composer (`mail.review_send`) and its reply cards end in one host-owned review of the exact message, drawn by the shell inside the card (`crates/shell/src/mail_review.rs`); Mail's agent can only propose a send (`mail.propose_send`). Only a trusted press and release of the review's Approve & Send control sends. Today that means physical touch on Android, through a reviewed Makepad patch (`tools/runtime-patches/makepad-trusted-user-input.patch`); desktop and accessibility approval are deferred. Developer mode and standing rules cannot authorize a send, and `mail.send` answers only `approval_required`. [Composed Mail cards](mail-composable-cards.md) describes the flow. -**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`). +**Standing rules** (`approvals/rules.rs`) can require recipients in contacts or in the thread, no attachments, a person's trigger, or count and amount limits; a fact that cannot be read fails the condition. A rule made from a sheet is capped at 20 uses a day by default, the everything-for-one-app rule lasts at most 60 minutes, and one tap turns all rules off. Only the person creates rules. "Recipients in contacts" uses Mail's data only after the person allows it (`approvals/contacts.rs`). -**What feeds the router.** 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. And octos, through the host-tool relay (`host_tools::init` installs it with `set_relay` at startup): every `host_tool` approval (a gated `confirm: host` app tool, raised only to the host connection) with the owning app, the exact arguments and the caller, and every `confirm: app` call, acknowledged first and handed to the owning app's sheet; the system chat hands its session's approvals over the same way. Every other approval octos raises on an app's peer session or one of its contexts (octos's own tools, a `peer/input` turn's included) goes to the same router as the app agent's call on its own app ([#155](https://github.com/OctoSense-org/OctoSense/pull/155)); the app's context hears only `approval/handled_by_host`. Only a host without a router leaves them to the app: then developer mode answers the covered apps' (`crates/app-peers/src/host_approvals.rs`, audited) and a script app's context declines them (`contained.rs`). +**Developer mode** (`crates/shell/src/dev_mode.rs`) is turned on only by the person: in Settings, with a typed phrase or the phone's developer-options gesture, or at launch with `OCTOSENSE_DEV_MODE` or `--dev-grant-all`. Release builds take only the flag, and store builds never turn it on. + +It covers all apps or the ones the person chose. Outside a developer profile it ends after 8 hours or at restart. While it is on, the shell shows a banner, audits every call in `logs/dev-audit.jsonl` and adds `dev.run` to the covered apps' peers. It never reaches external clients and cannot send mail. + +**Inputs.** The router gets the kernel's `host_tool` approvals through the relay; every other approval on an app's peer or contexts, as that app agent's call (the app hears only `approval/handled_by_host`); the system chat's approvals; and the AI pane's calls to `confirm: host` tools. + +**Deadlines.** A request the shell holds expires after 10 minutes: it is denied, never approved, and stays visible until dismissed. If its turn is still running 30 seconds later, the broker interrupts it. External clients' requests never expire in the shell. + +**Audit.** Each decision is one line in `logs/approvals-audit.jsonl`, with a digest of the arguments rather than the arguments. + +**First-use consent** (`approvals/consent.rs`) shows what an agent may read and use and where its model runs. A native module (`consent_for_module`) or a script app (`consent_for_contained`) gets its agent only after the person allows it. ## 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): +Every app gets one host-owned layout, declared in its manifest's `storage` block and computed only by the shell (`crates/shell/src/app_storage/`, ADR 0004 §11): ``` /apps// the app's jail (App Hub's jail root; a native app's sandbox root) @@ -360,18 +365,15 @@ One host-owned layout for every app, declared by its manifest (`storage` block) /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. A peer created since the octos 665209e5 pin gets it as `peer/prepare`'s `cwd` (`host_tools::agent_workspace`, recorded beside its host token); a peer created before keeps the kernel-provisioned workspace, because octos resumes a peer only under the workspace it was made with. The folder name and the memory namespace tag are still two different hashes of one account ([#139](https://github.com/OctoSense-org/OctoSense/issues/139)). -- **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. -- **The manifests reach the host** (`app_storage/lifecycle.rs`): every `native-apps.json` `storage` block at startup (the generated `NativeApp::storage`), and a script app's `manifest.json` block at install and at every launch (an installed app's `//bundle/`, a system app's newest `.system///`), parsed with `StorageSpec` and recorded with `Storage::set_spec`; `Storage::open` lays out the jail, account folders, `common/`, `cache/` and `secrets//` from it for the module handoff (`module_host.rs`), a process app's sandbox (`clients::sandbox_policy`) and a Card launch. App Hub's manifest schema at its pin accepts `storage.max_bytes`, `accounts`, `agent_workspace` and `cache_max_bytes` (no `external`, which is for reviewed native apps). Mail declares `accounts: true`, so its agent acts for its active account (the one the person signed in to last, from Mail's host service; `app_storage::lifecycle::contained_account_in`); every other script app acts for the device. -- **Quotas**: when a native app opens, a background thread measures its jail and `cache/` (never following symlinks) and logs a warning over `storage.max_bytes` / `cache_max_bytes`; a script app's isolate enforces its own. -- **Accounts** come from the places that have them: an app's assistant service (`OctosAppService::set_account`: Rinx's Matrix login and logout) reports every change through `app_peers::storage::account_changed`, and Mail's host service reports `mail.add_account` / `mail.remove_account` (`octosense_mail_service::AccountEvent`). Signing in opens the account's folder and lifts its suspension, before the broker resumes the peer. **Sign-out** (`Some(a)` → `None`) suspends the agent and never closes it: the broker closes the account's request contexts, `Storage::sign_out` makes the workspace answer `SignedOut`, tool calls are answered `signed_out`, no `peer/input` turn starts, and a suspended account's peer is not prepared. An account switch (`Some(a)` → `Some(b)`) signs `b` in and leaves `a` alone. An app without accounts acts for the device, which never signs out. -- **Removing an account** (Mail) deletes its folder; **uninstalling** (App Hub removes the jail; the shell sees the jail gone in `installed_app_changed`) deletes `secrets//` and its vault items. Both then erase the agent: the shell asks octos to `peer/purge` each (app, account) peer it recorded (octos#2649; in the background, `peer_purge_busy` retried), which erases its transcripts, memory and blackboard and frees the binding, and drops the record (`.peer`). The account stays suspended, recorded in the host's `secrets/.host/suspended.json` (0600, account ids hashed) so a restart keeps it, until it is added again; then it gets a new agent (the record is gone). Once the purge has succeeded, Settings → Approvals → App agents stops saying the agent's memory remains; when it fails, the record is kept for a later purge and Settings keeps saying so. A record from before records carried the peer's name is purged under the names the shell can derive (the app's label and id); a `peer_not_found` there is a failure, never success. **Signing out** only suspends: signing in again resumes the same agent. App Hub has no uninstall button at its current pin; when it gains one it must report the removal to the shell (as `take_completed_installs` does for an install). -- **Rinx** declares `accounts: true`, and the shell offers its module that storage (the `storage` capability), but Rinx does not claim it yet: its data stays where it is (below). What Rinx needs to take its per-account folders (the move goes through hagency-org/Rinx#37's rule that OctoSense pins only Rinx release tags; the pin is `v1.1.0`): in `RinxModule::create`, `octosense_app_peers::storage::claim("rinx", &handles.scope.to_string())` and keep the handle; after login, write what its agent should read (exported threads, shared attachments) into `storage.account_folder(Some())`; keep the Matrix store and tokens through `storage.secrets()` or under `secrets/rinx/`, never in the account folder; keep `app_data_dir()` for existing data until the migration. No shell API changes are needed. -- **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 (ADR 0004 §11, through [hagency-org/Rinx#37](https://github.com/hagency-org/Rinx/issues/37)'s release-tag rule); 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). +- **The OctoSense home** is the platform's app data directory on a phone, else `~/.octosense` (`crates/shell/src/octosense/paths.rs`). Symlinked components are refused, and on Unix the directories are 0700. +- **The account hash** is 128 bits of a domain-separated SHA-256 of the normalized account id (`account_hash`). It names every account folder, so changing it needs a migration. +- **The `storage` block** (`accounts`, `agent_workspace`, `max_bytes`, `cache_max_bytes`; `external` for native apps) is read from `native-apps.json` at startup and from a script app's manifest at install and launch (`app_storage/lifecycle.rs`). Mail declares `accounts: true`. +- **Secrets** are never under `apps/` (`app_storage/secrets.rs`). macOS and iOS keep them in the keychain; elsewhere each key is a file in `secrets//`, owner-only (0600) on Unix. Script apps reach theirs only through host services and host sheets. +- **The startup check** (`app_storage/check.rs`) refuses a workspace that is a symlink, or that links into or contains the secrets, until a later start finds it clean. Nothing is deleted. + +Rinx (through `OctosAppService::set_account`) and Mail's host service report accounts. Removing an account deletes its folder; uninstalling deletes the app's jail, secrets and keychain items. Then the shell asks octos to `peer/purge` each recorded peer (`crates/app-peers/src/purge.rs`), which erases its transcripts, memory and blackboard. The account stays suspended (`secrets/.host/suspended.json`) until it is added again, with a new agent. + +The kernel's core dir, `~/.octosense/octos-home/.octos` on the desktop and `/octos-home/.octos` on a phone (`crates/kernel/src/dirs.rs`), holds its profile, sessions, blackboards and memory. Provider keys belong to the `llm` host service ([ai-services.md](ai-services.md#ai-providers-and-the-llm-host-service)). **Planned:** Rinx, which does not claim this storage yet, moves its data under `apps/rinx/` (ADR 0004 §11). ## 7. Trust boundaries and isolation @@ -380,14 +382,14 @@ One host-owned layout for every app, declared by its manifest (`storage` block) 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 | +------------------------------------------+ | - | +------------------+ | + | checks every call: grants, consent, approvals, budgets, audit | + | +------------------+ +------------------------------------------+ | + | | native modules | | Card runner: script apps in isolates | | + | | reviewed, same | | (jail, quota, host.request by grant) | | + | | memory: trusted | +------------------------------------------+ | + | +------------------+ | +-------|------------------------------------------|-----------------------+ - | hub (loopback) | OUP, host token + | hub (loopback), secret per launch | OUP, host token +-------v----------+ +--------v--------------------+ | process apps | | octos kernel | | (OS sandbox: | | workspace fence per peer | @@ -395,106 +397,106 @@ One host-owned layout for every app, declared by its manifest (`storage` block) +------------------+ +-----------------------------+ ``` -| 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) | macOS on main (`crates/shell/src/sandbox/macos.rs`, a Seatbelt profile, built and tested; [#130](https://github.com/OctoSense-org/OctoSense/pull/130)); Linux Landlock and seccomp on main (`crates/shell/src/sandbox/linux.rs`), validated on a real kernel on 2026-09-30 ([#138](https://github.com/OctoSense-org/OctoSense/issues/138), fixes [#199](https://github.com/OctoSense-org/OctoSense/pull/199), [#200](https://github.com/OctoSense-org/OctoSense/pull/200), [#202](https://github.com/OctoSense-org/OctoSense/pull/202)); their tests run in local CI's Linux-host job `linux-host / sandbox` ([local-ci.md](local-ci.md)), not in GitHub CI; Windows planned ([#137](https://github.com/OctoSense-org/OctoSense/issues/137)): a process app there runs with the user's rights | -| App ↔ kernel | apps never speak the kernel protocol and **never see the host token**: modules get the peer link or a scoped `OctosAppService`, script apps a host service, process apps 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)) | +| Boundary | What holds it | +| --- | --- | +| Script app ↔ shell | The Card runner's isolate, jail and quota; `host.request` only for granted families | +| Native module ↔ shell | Nothing in memory: review of first-party code, and panic containment | +| Process app ↔ shell | Its own address space and an OS sandbox: Seatbelt on macOS, Landlock and seccomp on Linux. **Not yet:** Windows. | +| App ↔ kernel | No app speaks OUP or sees the host token | +| Peer ↔ peer | octos gives each peer its own workspace (overlaps refused), memory namespace and transcript | +| Agent ↔ secrets | Secrets outside every jail and workspace; the startup check | +| Agent ↔ Mail delivery | No agent tool or generated card sends: only the host's review of the exact message, approved by trusted physical touch | +| External client ↔ kernel | The external token, method and tool allowlists, `Host` and `Origin` checks | -**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. For app tool calls (`crates/shell/src/host_tools/`): the grant by (owning app, tool) and caller, consent, a signed-out account, each call's arguments against the tool's declared `input_schema` (and 64 KiB), each result against its `output_schema` and 256 KiB, each calling agent's budget (calls per turn and per day, `native-apps.json` `agent.budget` or 32 and 1000), the confirmation, once per occurrence, nothing after a cancel. Rate limits beyond the budgets and background policy are still to come. The actions a person takes in an app's cards do not pass through this relay: they take the app's own path (ADR 0004 §4), so these checks never cover them. +**The process sandbox** (`crates/shell/src/sandbox/`) is built from the entry's `sandbox` and `storage` blocks. It closes the person's home and volumes except the app's jail, its secrets folder and its reviewed `external` grants (the Terminal's `home:rw`), and keeps everything the next build reads or runs read-only. `network: none` leaves only the hub's port, `processes: false` forbids fork and exec, and the app inherits only an allow-list of the shell's environment, never a key or token. -**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. +**What the shell checks** on each tool call is the relay's list ([section 4](#the-relay)); a script app's own `octos.*` calls are held to exact service names, 32 KiB of text and 2 MiB replies. **What nobody can check** is 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 -This **historical ADR 0004 sketch is not an implemented send workflow**. Calendar ships `calendar.add_event`; Mail now provides account-scoped reads, draft/proposal operations and card publication, but declares no shareable agent tool `mail.send`. ADR0007 supersedes the sketch's hypothetical standing-rule sending: current Mail requires physical approval of the exact host review. The diagram below illustrates the proposed cross-app relay shape, not today's Mail approval contract. For the implemented reply flow, see [Composed Mail cards](mail-composable-cards.md). +This is ADR 0004's original sketch of cross-app work: Calendar's real `calendar.add_event`, then a cross-app `mail.send`. The Mail half is **not built**, and ADR 0007 has replaced how it would be approved: no standing rule or developer mode may approve a Mail send, and each message needs the host's review and a physical touch ([section 5](#5-approvals)). The diagram keeps the sketch's relay path and marks what ADR 0007 replaced. [Composed Mail cards](mail-composable-cards.md) describes how Mail sends today. ```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 C as Calendar's agent (peer) + participant R as Shell: approval 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) + Note over S,SH: octos delivers peer/input to the shell + SH->>C: turn/start (Calendar's tools, memory, account) C->>SH: peer/tool/call calendar.add_event SH-->>C: result - C->>SH: peer/tool/call mail.send x3 (caller: Calendar) - SH->>SH: grant check: Calendar granted mail.send? + C->>SH: peer/tool/call mail.send x3 (not built, caller: Calendar) + SH->>SH: grant check: was 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 + Note over R,P: ADR 0004 let a standing rule or a sheet approve this.
ADR 0007 replaced that: no rule or developer mode may approve a Mail send. + R->>P: the host's review of each exact message + P-->>R: approves each, by physical touch + SH->>M: send x3 (not built) 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" + 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`. | On main: the shell's system chat ([#132](https://github.com/OctoSense-org/OctoSense/pull/132)), or a paired Talk to Octos client (the system assistant pane is separate from an app's Makepad `aichat` bus interface) | -| 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`; if the person has not allowed Calendar's agent yet, `agents.ask` shows them the first-use sheet and waits for the answer. | `peer_send_input`, `agents.list` and `agents.ask` on main, and relay grants implemented; the system agent's host tools are described in section 4; this example's Mail tool and grant remain unimplemented | -| 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. | On main ([octos#2567](https://github.com/octos-org/octos/pull/2567) in the pin; the broker starts the turn) | -| 4 | Calendar's agent calls `calendar.add_event` (its own tool, `act`: it runs without a sheet) 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. | Calendar action ( the `calendar` host service keeps the events in `/calendar/events.json`) and relay implemented; `mail.send` planned | -| 5 | The shell checks that Calendar's manifest was granted `mail.send` (at install for a script app); no second, agent-level consent. | Relay and App Hub admission checks implemented; this specific grant/tool is absent | -| 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 holds only once the person turned on "Use my contacts in approval rules"), 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 `host_tool` approvals through the relay, the path Calendar's destructive `calendar.remove_event` takes today; `mail.send` planned | -| 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 host service implemented (its legacy `mail.send` path also requires host review); its agent has `mail.notify`, while `mail.send` as an agent tool remains 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: Calendar's own window cannot list events yet (it needs an App Hub `calendar` capability); `calendar.notify` can put the event on the glance screen today | +- The system agent asks about ambiguity ("two Edwards?") rather than guessing. If Calendar's agent is not allowed yet, `agents.ask` shows the first-use sheet first. +- `calendar.add_event` is `act`, so it runs without a sheet. Calendar ships on the desktop and the phone. +- The grant would use the existing mechanism: `mail.send` as a dotted name in Calendar's `agent.tools`, granted at install. Mail declares no such tool. +- ADR 0004 had the approval router decide each send, so a standing rule such as "recipients in my contacts" could approve it, and otherwise each invitation got its own sheet. ADR 0007 replaced that for Mail: whoever proposes a send, it ends in the host's review of the exact message ([section 5](#5-approvals)). Mail's agent can only propose a send (`mail.propose_send`). +- Mail's host service sends with the account the person signed in to; the password never reaches the agent. Its `mail.send` method now answers only `approval_required`. +- A send with an unknown outcome is never repeated automatically; a retry needs a fresh review and approval. +- **Not yet:** Calendar's window cannot list events; `calendar.notify` can put the event on the glance screen. ## Where the code and the ADRs disagree -Found while writing this page (2026-09-28), rechecked against `main` on 2026-09-29 (the ADR 0004 gap review) and again on 2026-10-02; items marked fixed were fixed in code or in the ADR text since. - -1. **Process apps beyond the Terminal.** Fixed: the opt-in Sheets and Reference are `module` on every target, and the generator refuses plain `process` on Linux (ADR 0004 §2). -2. **Restart of a dead process app.** Fixed ([#130](https://github.com/OctoSense-org/OctoSense/pull/130)): an unexpected death keeps the tile, closed with a Restart (`ClientSlot::stops_in_place`; test `an_unexpected_death_keeps_the_tile_closed_with_restart`). -3. **Agent workspace = account folder.** Partly fixed: a peer created since the octos 665209e5 pin gets its account folder as the `peer/prepare` `cwd`; a peer created before keeps the kernel-provisioned workspace, because octos resumes a peer only under the workspace it was made with (moving it needs a migration octos does not offer). The account folder name (`account_hash`, SHA-256) and the memory namespace tag (FNV-1a in `broker.rs`) are still different hashes of the account; changing the tag would re-key every app's memory ([#139](https://github.com/OctoSense-org/OctoSense/issues/139)). -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. Fixed in the text: ADR 0003 carries a dated amendment (2026-09-29), as do ADR 0002 §12 and its amendment. (Fixed, G13: when `enforce` refuses a foreign policy or the person's own octos home, or the policy does not read back, the kernel does not start and every consumer is told why; and each app peer keeps exactly its granted `generic_tools`, never the shell.) -5. **Stale context lines.** Fixed: ADR 0004's context table and the README layout tables no longer list the native News, Maps and Photos modules ADR 0004 step 1 deleted (#113), and [ai-services.md](ai-services.md) now describes `main` on 2026-09-28 (the Card runner's `octos` service from #106, consent from #120; the current `Policy::contained_gate` defaults to `ContainedGate::Consent`, not globally off). -6. **Command execution "turned on in Settings".** Fixed: Setup › Assistant › Command execution sets it (#132), and the system chat registers `terminal.run` on the system session while it is on. -7. **`host::processes_available()`'s test.** Fixed: `the_desk_knows_where_it_runs` checks the phone targets as well as `wasm32`. -8. **Approvals, ADR 0004 §8.** Every app tool call reaches the shell through `peer/tool/call` and every gated one the router (above). No app registers its own `confirm: app` sheet yet (Rinx needs to hand its send sheet over through `OctosAppService::set_confirm_sheet`; a process app's is registered when its peer link opens, and none uses one), so such a call would wait and be refused. The one `confirm: app` tool on main is the shell's own `agents.ask`, which the system chat holds itself. 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. (Fixed: `storage.accounts` defaults to `false` in both app storage and the consent sheet, which now reads `StorageSpec`.) -10. **Developer mode, ADR 0004 §13.** Fixed: `dev.run` is registered on covered apps' peers and run by the shell. Fixed: Settings chooses the apps it covers (the desktop's Setup › Developer options, a phone's About phone › Developer options), and a phone reveals Developer options with seven taps on Build number, then turns it on there with a confirmation. Open: in-process modules still show their own confirmation sheets. -11. **Questions, ADR 0004 §6 (decided 2026-09-29).** Agents ask with octos's `ask_user_question` and the shell routes each question to the app's conversation or the system chat; `host.ask` is dropped. Fixed: the broker forwards an app peer's `user_question/requested` to the shell's request model (`crates/shell/src/questions/`), which routes it by the turn's origin to the app's conversation or the system chat; only the person answers, on a shell surface (cards never answer). An agent asks only when its `generic_tools` list `ask_user_question`. -12. **Process sandboxes, ADR 0004 §3.** Fixed for Linux: Landlock and seccomp were validated on a real kernel on 2026-09-30 (#138, with the fixes #199, #200 and #202), and their tests run in local CI's `linux-host / sandbox` job, not in GitHub CI. Open: Windows is not built (#137). -13. **App-agent follow-ups from the 2026-09-29 review, rechecked 2026-10-01.** The old blanket claims that approvals are dropped, every app uses `device`, and failed input starts are only logged no longer describe the implementation. `broker.rs::on_user_question`/approval routing hand prompts to the host; `contained.rs::account_of` consults the host account source; input-start failures use `peer/input/reject`; `driver_of`/`take_over` coordinate multiple brokers for one peer. The `broker` tests exercise these paths. The contained `host.request` adapter still returns a collected turn reply rather than a script-visible streaming subscription; native contexts and the shell's chat surfaces have event subscriptions. `implemented_by: "app"` script tool dispatch, bundle `AGENT.md` prompt loading and automatic skills/triggers remain unimplemented. Read the [walkthrough](architecture-walkthrough.md) for the exact current boundaries. +Each item is an ADR decision that the code at HEAD does not follow. + +1. **Native agents with `agent_workspace: "none"` still get a workspace.** ADR 0004 §11 says such an agent reads no files. But `host_tools::agent_workspace_in` gives every native app with `octos.*` services its account folder as the peer's workspace, whatever `agent_workspace` declares, and the shell registers `files.*` on every peer with a workspace (Unix). So Calculator, Clock, Notes, Reminders, Weather and the Terminal get a folder and `files.*`. The declaration applies only to script apps and to the person's lane's read-only view of the folder (`context_reads_account`). +2. **Older peers and two hashes.** ADR 0004 §11 makes the account folder every peer's workspace. A peer recorded without one resumes with the account folder if octos accepts it, else with its kernel-provisioned workspace. The folder name (SHA-256) and the memory tag (FNV-1a, `account_tag`) are still two hashes of one normalized account. +3. **Secrets in a platform vault.** ADR 0004 §11 asks for the OS keychain where there is one. Only macOS and iOS use it. Windows, Linux, Android and OpenHarmony keep each key in a plain file, owner-only (0600) on all but Windows. +4. **Rate limits and background policy.** ADR 0004 §3 lists both among the relay's checks; the relay enforces budgets only. +5. **After the fact.** ADR 0004 §8 promises a send queue with an undo window, and auto-approvals audited with their arguments. There is no queue or undo, and the audit keeps a digest. +6. **Per-client file grants.** ADR 0004 §11 narrows the host read tools by per-client grants; no manifest field declares them yet. +7. **Rinx.** ADR 0004 §9 confirms Rinx's send tool on its own `confirm: app` sheet, §11 moves its data under `apps/rinx/`, and §13 lets developer mode override every app's sheet. At its pinned tag Rinx declares no agent tools, registers no sheet with the router, keeps its own data folder, and its send sheet still asks in developer mode. +8. **The Windows sandbox.** ADR 0004 §3 gives Windows process apps an AppContainer. It is not built, so they run with the person's rights, and `terminal.run` is never offered there. +9. **Process apps in release packages.** ADR 0004 §2 ships their binaries; release packages ship only `octosense` and the kernel (`desktop/packaging/release.json`). +10. **Mail sending.** ADR 0004 §8 lets a standing rule answer an outward call such as `mail.send`, and §13 lets developer mode override every approval. The code follows ADR 0007 instead: a Mail send needs the host's review and a physical touch whatever the rules or developer mode say, and ADR 0004 does not mention the exception. +11. **The in-process Terminal still offers `run`.** ADR 0004 §10 makes the in-process Terminal read-only. The shell, though, replaces its module's read-only executor with one that also offers `run` (`host_executor` in `crates/shell/src/module_host.rs`, kept by a test). So wherever the desktop runs the Terminal in process (Linux without Vulkan and Wayland, or switched to a module), the desktop's AI pane can still type a command, behind the approval router. The system agent's `terminal.run` follows the ADR: it needs the Terminal as a sandboxed process. ## 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) | -| Which apps have an agent; the system agent's `agents.list` and `agents.ask` | [`crates/shell/src/apps.rs`](../crates/shell/src/apps.rs) (`agent_apps`), [`crates/shell/src/agents.rs`](../crates/shell/src/agents.rs) | -| The system chat and the "Ask " panel | [`crates/shell/src/system_chat/`](../crates/shell/src/system_chat/mod.rs), [`crates/shell/src/app_chat/`](../crates/shell/src/app_chat/mod.rs) | -| Host-tool relay: app tools, script apps' executor, host read tools, toolbox | [`crates/shell/src/host_tools/`](../crates/shell/src/host_tools/mod.rs) (`relay.rs`, `script_apps.rs`, `files.rs`, `toolbox.rs`), [`crates/ai-host/src/toolbox_peers.rs`](../crates/ai-host/src/toolbox_peers.rs), [`crates/toolbox`](../crates/toolbox/README.md) | +| Hosting, host services, which apps have an agent | [`crates/shell/src/apps.rs`](../crates/shell/src/apps.rs) | +| Process apps, the hub, sandboxes | [`crates/shell/src/clients.rs`](../crates/shell/src/clients.rs), [`hub.rs`](../crates/shell/src/hub.rs), [`sandbox/`](../crates/shell/src/sandbox/mod.rs) | +| In-process modules, panic containment | [`crates/shell/src/module_host.rs`](../crates/shell/src/module_host.rs) | +| Kernel service, Talk to Octos, frame router, the system agent's tools | [`crates/kernel/src/`](../crates/kernel/README.md) (`launch.rs`, `lib.rs`, `kernel.rs`, `dirs.rs`, `network.rs`, `router.rs`, `system_tools.rs`) | +| Shell AI entry point; script apps' `octos` service; the module peer link | [`crates/ai-host/src/`](../crates/ai-host/README.md) (`lib.rs`, `contained.rs`, `module_peer.rs`) | +| App peers: contract, broker, launch, injection, guidance, purge | [`crates/app-peers/src/`](../crates/app-peers/README.md) | +| The system agent's host tools; Mail's events | [`crates/shell/src/agents.rs`](../crates/shell/src/agents.rs), [`agent_events.rs`](../crates/shell/src/agent_events.rs), [`system_chat/grants.rs`](../crates/shell/src/system_chat/grants.rs) | +| The system chat and the "Ask <app>" panel | [`crates/shell/src/system_chat/`](../crates/shell/src/system_chat/mod.rs), [`app_chat/`](../crates/shell/src/app_chat/mod.rs) | +| The peer link | [`crates/shell/src/peer_link/`](../crates/shell/src/peer_link/mod.rs) | +| Host-tool relay, executors, host read tools, `dev.run`, toolbox | [`crates/shell/src/host_tools/`](../crates/shell/src/host_tools/mod.rs), [`crates/toolbox`](../crates/toolbox/README.md) | +| AI services bus | [`crates/shell/src/ai_bus.rs`](../crates/shell/src/ai_bus.rs) | | Agents' questions | [`crates/shell/src/questions/`](../crates/shell/src/questions/mod.rs) | -| Glance cards: the service, notices, tiles, panel, card window, in-card chat, toasts | [`crates/shell/src/glance.rs`](../crates/shell/src/glance.rs), [`glance_notice.rs`](../crates/shell/src/glance_notice.rs), [`glance_card.rs`](../crates/shell/src/glance_card.rs), [`glance_panel.rs`](../crates/shell/src/glance_panel.rs), [`glance_sheet.rs`](../crates/shell/src/glance_sheet.rs), [`glance_chat.rs`](../crates/shell/src/glance_chat.rs), [`shell/notifications.rs`](../crates/shell/src/shell/notifications.rs); [`crates/l0-chat`](../crates/l0-chat/README.md) | -| System apps' agent tools and host services | `apps//bundle/tools.json`; [`apps/mail/host-service`](../apps/mail/host-service/src/lib.rs), [`apps/calendar/host-service`](../apps/calendar/host-service/src/lib.rs), [`apps/news/host-service`](../apps/news/host-service/README.md) | -| Process sandboxes | [`crates/shell/src/sandbox/`](../crates/shell/src/sandbox/mod.rs) (`macos.rs`, `linux.rs`) | -| 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](https://github.com/octos-org/octos/blob/main/docs/OCTOS_UI_PROTOCOL_CHANGE_REQUEST_UPCR_2026_035_PEER_HOST_TOOLS.md) ([octos#2567](https://github.com/octos-org/octos/pull/2567)), [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) | +| Approvals, consent, contacts, audit | [`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, accounts | [`crates/shell/src/app_storage/`](../crates/shell/src/app_storage/mod.rs) | +| Glance cards and the in-card chat | [`crates/shell/src/glance.rs`](../crates/shell/src/glance.rs), [`glance_notice.rs`](../crates/shell/src/glance_notice.rs), [`glance_card.rs`](../crates/shell/src/glance_card.rs), [`crates/l0-chat`](../crates/l0-chat/README.md) | +| System apps, their tools and host services | [`desktop/system-apps.json`](../desktop/system-apps.json), [`phone/system-apps.json`](../phone/system-apps.json), `apps//bundle/tools.json`, `apps//host-service/` | + +Code outside this repository, at the revisions `Cargo.toml` pins: + +| What | Where | +| --- | --- | +| Makepad: the `OctosPeer` client; each native app's agent code | [`libs/ai/services/src/peer.rs`](https://github.com/OctoSense-org/makepad/blob/c155f61d0e1600d2ec474209374444a38a09a470/libs/ai/services/src/peer.rs), `apps//src/view.rs` | +| The Terminal's own-agent link, a reviewed patch this repository applies to Makepad | [`tools/runtime-patches/makepad-terminal-own-agent.patch`](../tools/runtime-patches/makepad-terminal-own-agent.patch) | +| octos: the lanes' shared history (20 rows, 16 KiB); `peer_send_input` (64 KiB) | [`crates/octos-cli/src/peers/shared_history.rs`](https://github.com/octos-org/octos/blob/056173e85b150e387805fc307fe231064ac1ed35/crates/octos-cli/src/peers/shared_history.rs), [`crates/octos-agent/src/tools/peer_send_input.rs`](https://github.com/octos-org/octos/blob/056173e85b150e387805fc307fe231064ac1ed35/crates/octos-agent/src/tools/peer_send_input.rs) | +| octos: host-managed serve, app peers, peer host tools | [`docs/HOST_MANAGED_SERVE.md`](https://github.com/octos-org/octos/blob/056173e85b150e387805fc307fe231064ac1ed35/docs/HOST_MANAGED_SERVE.md), [UPCR-2026-034](https://github.com/octos-org/octos/blob/056173e85b150e387805fc307fe231064ac1ed35/docs/OCTOS_UI_PROTOCOL_CHANGE_REQUEST_UPCR_2026_034_HOST_APP_PEERS.md), [UPCR-2026-035](https://github.com/octos-org/octos/blob/056173e85b150e387805fc307fe231064ac1ed35/docs/OCTOS_UI_PROTOCOL_CHANGE_REQUEST_UPCR_2026_035_PEER_HOST_TOOLS.md), [UPCR-2026-036](https://github.com/octos-org/octos/blob/056173e85b150e387805fc307fe231064ac1ed35/docs/OCTOS_UI_PROTOCOL_CHANGE_REQUEST_UPCR_2026_036_HOST_MANAGED_SERVE.md) | +| App Hub: the kernel tools a script app may keep (`KERNEL_TOOLS`) | [`crates/app-policy/src/policy.rs`](https://github.com/OctoSense-org/OctoSense-App-Hub/blob/0d5b47a2ae9eb98020feca26b7c895a3cf797dc1/crates/app-policy/src/policy.rs) | diff --git a/docs/architecture.zh-CN.md b/docs/architecture.zh-CN.md index ba59d3e4..374ceab1 100644 --- a/docs/architecture.zh-CN.md +++ b/docs/architecture.zh-CN.md @@ -2,17 +2,9 @@ [English](architecture.md) | 简体中文 -OctoSense 的进程、Agent、工具、审批、存储与信任边界。具体调用和所有权见[代码导读](architecture-walkthrough.zh-CN.md)。依赖版本以 [Cargo.toml](../Cargo.toml) 和 [native-runtime.lock.json](../native-runtime.lock.json) 为准;下方带日期的设计记录保留早期决策。 +本文完整说明 OctoSense 是怎样构建的,并给出每个部分背后的代码。阅读前请先了解 README 的[关键概念](../README.zh-CN.md#关键概念)和[整体如何运作](../README.zh-CN.md#整体如何运作)。各类应用能调用什么、如何在本地运行 AI 服务,见 [ai-services.zh-CN.md](ai-services.zh-CN.md);想按顺序读源码,请看[代码导读](architecture-walkthrough.zh-CN.md)。 -**Mail 回复实现:** [ADR0007](adr/0007-composable-mail-action-cards.zh-CN.md) 的草稿/编辑/聊天/审核适配器已在源码中实现。Mail Agent 可以提议草稿与审核,不能批准或发送。开发者模式也必须经过宿主审核控件;当前只有 Android 实体触摸能授权。[当前代码与验证条件](mail-composable-cards.zh-CN.md)取代下文较早的 Mail 演示/发送路径描述。其中记录了 OnePlus 6 上 DeepSeek 与 MiniMax 的限定范围测试;完整设备/UX 矩阵和桌面/无障碍发送审批仍未完成。 - -每条陈述都标明状态: - -- **已在 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、跨应用协作与审批,已接受,并已于 2026-09-29 实现,后续事项除外)。助手的各项服务、各类应用目前能调用什么、如何在本地运行,见 [ai-services.zh-CN.md](ai-services.zh-CN.md);本文不再重复。 +背后的决策是 [ADR 0001](adr/0001-one-octosense-repository.md)(英文,一个仓库)、[ADR 0002](adr/0002-event-driven-app-agents.md)(英文,事件驱动的应用 Agent;Proposed,部分已实现)、[ADR 0003](adr/0003-shared-octos-client-access.md)(英文,Talk to Octos)、[ADR 0004](adr/0004-native-apps-hosting-and-peers.md)(英文,原生应用、应用 Agent、跨应用协作与审批)和 [ADR 0007](adr/0007-composable-mail-action-cards.zh-CN.md)(带草稿、对话和宿主审批发送的邮件卡片;进行中)。正文描述代码的现状;尚未实现的部分标为**尚未实现**或**规划中**。 ## 目录 @@ -34,22 +26,22 @@ OctoSense 的进程、Agent、工具、审批、存储与信任边界。具体 ![OctoSense AI 服务:一次已授权的跨应用调用](images/ai-services-cross-app-call.png) -*示意图(生成)。实线表示已在 main 上;虚线表示计划中或进行中(进程应用的 peer link 自 [#130](https://github.com/OctoSense-org/OctoSense/pull/130) 起已在 main 上,Terminal 正在使用;`peer/input` 和宿主工具自锁定 octos 665209e5 起已在 main,octos#2567)。图与下文不一致时以文字为准:例如 `peer_send_input` 把消息放入 peer 的收件箱,peer 在下一轮处理它。* +
文字版(Mermaid) ```mermaid flowchart LR person(["用户"]) subgraph shellp["OctoSense Shell 进程(桌面端,或手机上的 Home)"] wm["窗口管理器、启动器、面板、
系统对话、glance 面板
crates/shell"] - mods["进程内原生模块
App Hub、Rinx、(AppCard)"] + mods["进程内原生模块
App Hub、Rinx、Notes、Clock 等、(AppCard)"] runner["App Hub Card runner
脚本应用各在隔离环境中"] router["审批路由
crates/shell/src/approvals"] - aihost["ai-host + app-peers broker
到内核的宿主连接"] - relay["宿主工具中继
crates/shell/src/host_tools"] + aihost["ai-host + app-peers 代理
到内核的宿主连接"] + relay["宿主工具中转
crates/shell/src/host_tools"] svc["应用宿主服务
mail、calendar、news;通知"] bus["AI 服务总线
crates/shell/src/ai_bus.rs"] end - term["进程应用(桌面端)
Terminal"] + term["进程应用(桌面端)
Terminal、Task"] subgraph kern["octos 内核(每个 Shell 一个)"] sys["系统 Agent 会话
(profile _main)"] peers["应用 peer
每个(应用,账号)一个"] @@ -59,7 +51,8 @@ flowchart LR wm --- mods wm --- runner term <-->|"hub(回环 WebSocket)"| wm - mods -- "注入的 OctosAppService" --> aihost + term -- "peer link,走 hub 连接" --> aihost + mods -- "注入的 OctosAppService 或 peer link" --> aihost runner -- "host.request(octos.*)" --> aihost aihost <-->|"OUP over stdio
(或宿主 token 的 WebSocket)"| kern sys -- "peer_send_input / 黑板" --> peers @@ -73,92 +66,73 @@ flowchart LR bus --> router ``` -``` -+--------------------------- OctoSense Shell 进程 -------------------------------+ -| 窗口管理器 / 启动器 / 面板 审批路由 AI 服务总线 | -| 进程内模块:App Hub(+ 运行脚本应用的 Card runner)、Rinx、(AppCard) | -| 宿主工具中继 -> 应用宿主服务(mail、calendar、news、通知) | -| ai-host + app-peers broker == 宿主连接 ==+ | -+--------------------------------------------|-----------------------------------+ - ^ hub:回环 WebSocket | OUP:stdio(默认)或 - | | 带宿主 token 的 WebSocket -+-------+---------+ +--------------v--------------------------+ -| 进程应用 | | octos 内核(子进程;OHOS 上在进程内) | -| Terminal 等 | | 系统 Agent 会话 | -+-----------------+ | 应用 peer:每个(应用,账号)一个 | - | 回合执行与工具分发 | - Talk to Octos 客户端 ----> +-----------------------------------------+ - (外部 token、允许列表、需手动开启) -``` +
+ +每个 Agent 都是同一个内核中的一个会话;Agent 对应用工具的每一次调用,和每一次审批一样,都要经过 Shell。[第 8 节](#8-完整示例用邮件发送会议邀请)会沿着第二张图里的那次调用走一遍。 ## 1. 各平台的进程 ### Shell -每台设备一个 Makepad 进程:桌面端(`desktop/`,包 `octosense`)或手机上的 Home(`phone/`,包 `octosense-home`),两者都链接同一个 Shell crate `crates/shell`([ADR 0001](adr/0001-one-octosense-repository.md))。Shell 拥有窗口管理器、启动器、宿主面板、审批路由、应用存储以及内核的宿主连接。 +每台设备一个 Shell 进程:桌面端(`desktop/`,package `octosense`)或手机上的 Home(`phone/`,package `octosense-home`),两者都由 `crates/shell` 构建([ADR 0001](adr/0001-one-octosense-repository.md),英文)。它拥有窗口管理器、启动器、宿主面板、审批路由、应用存储,以及内核的宿主连接。 ### 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`): +内核是一项 Shell 服务,每个 Shell 至多一个,位于 [`crates/kernel`](../crates/kernel/README.zh-CN.md)(package `octosense-kernel`),通过 [`crates/ai-host`](../crates/ai-host/README.md) 访问。`launch::resolve`(`crates/kernel/src/launch.rs`)决定它如何运行: -| 平台 | 内核 | 如何启动 | -| --- | --- | --- | -| 桌面端(macOS;Windows 和 Linux 未经测试) | 子进程:嵌入方的 `Options::program`,否则为 `OCTOS_APP_CORE_BIN` 指定的 `octos` 二进制,否则为 Shell 旁随附的 `octos-kernel`(只有收据中的版本与固定的 octos 版本一致时才运行) | `serve --stdio --data-dir `;没有二进制就没有内核(不在 `PATH` 或工作目录中查找)。 | -| 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 | 无 | 提供方配置会保存;没有应用能用上助手 | +| 平台 | 内核 | +| --- | --- | +| 桌面端(macOS;Windows 和 Linux 未经测试) | 子进程,`serve --stdio`:依次取 Shell 的 `Options::program`、`$OCTOS_APP_CORE_BIN`、Shell 旁随附的 `octos-kernel`(仅当其收据记录的正是锁定的 octos 版本时)。Shell 从不搜索 `PATH`。 | +| Android | 子进程:APK 中的 `liboctos.so serve --stdio`,由 [`tools/kernel-artifact.py`](../tools/kernel-artifact.py) 构建 | +| 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()` 启动新一代。见[内核崩溃意味着什么](#内核崩溃意味着什么)。 +- **按需启动。** 第一次 `connect()` 启动内核,之后的使用方加入同一代内核。新一代内核要等旧内核释放数据目录后才启动。 +- **重启。** 提供方变化后,`llm` 宿主服务调用 `restart()`。所有连接以 `CloseReason::Restarted` 结束,使用方重新连接。 +- **空闲停止。** Talk to Octos 关闭时,最后一个连接关闭,内核就停止。每个存活的代理都持有一个连接,所以只要有应用 Agent 已准备好,内核就一直运行。 +- **退出与崩溃。** Shell 退出或崩溃时,内核会在 stdin 上读到 EOF。内核崩溃会以 `CloseReason::Exited` 结束所有连接,下一次 `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。** 目前的清单: +原生应用是经过审查的第一方 Rust crate,只在 [`native-apps.json`](../native-apps.json) 中声明。`tools/native_apps.py` 据此生成 `crates/shell/src/native_apps.rs` 和 Cargo 条目,CI 以 `--check` 运行它。 -| 应用 | macOS / Windows 上的托管 | Linux | Android、iOS、OpenHarmony | 桌面端 / 手机 | 助手 | +| 应用 | macOS、Windows | Linux | 手机 | 桌面端 / 手机构建(`shells`) | Agent | | --- | --- | --- | --- | --- | --- | -| App Hub(`apphub`:商店和 Card runner) | 进程内 | 进程内 | 进程内 | 默认 / 默认 | – | -| Rinx | 进程内 | 进程内 | 进程内 | 默认 / 默认 | 获授权四个 `octos.*` 服务 | -| Terminal | **独立进程** | Vulkan 构建且在 Wayland 会话中时为独立进程,否则进程内 | 进程内 | 默认 / 关闭 | 有自己的 Agent,只有只读工具(`own_tools`);输入命令是系统 Agent 的 `terminal.run`(`confirm: host`、`auto_approvable: false`) | -| Calculator、Clock、Notes、Reminders、Weather | 进程内 | 进程内 | 进程内 | 默认 / 默认 | 获授权四个 `octos.*` 服务;它们的只读工具也提供给系统 Agent(`system_tools`) | -| Sheets、Reference | 进程内 | 进程内 | 进程内 | 可选 / `mobile-apps` | – | -| AppCard | 进程内 | 进程内 | 进程内 | 可选 / 可选 | 自己的内核连接 | - -因此**只有 Terminal 是进程应用**,而且只在桌面端;`tools/native_apps.py` 拒绝 Linux 上的普通 `process`(只允许 `process-if-vulkan`),所以没有 Vulkan 和 Wayland 的 Linux 上所有应用都在进程内运行。App Hub 留在进程内,因为它承载所有脚本应用运行所在的 Card runner。Rinx 留在进程内:ADR 0004 §2 规定,等 peer link 和进程沙箱就绪后,再通过一次经评审的 `hosting` 改动把它移出。如今 peer link 的 Shell 一侧以及 macOS 和 Linux 的沙箱已在 main 上,但所锁定版本的 Rinx 通过注入的服务([第 3 节](#应用与它自己的-agent))而不是 peer link 访问它的 Agent,它的 `hosting` 在所有平台上仍是 `module`。 +| App Hub(商店、Card runner) | 模块 | 模块 | 模块 | default / default | – | +| Rinx | 模块 | 模块 | 模块 | default / default | 注入的服务 | +| Terminal | **进程** | 有 Vulkan 和 Wayland 时为**进程**,否则为模块 | 模块 | default / off | peer link;`terminal.run` 归系统 Agent | +| Calculator、Clock、Notes、Reminders、Weather | 模块 | 模块 | 模块 | default / default | peer link | +| Sheets、Reference | 模块 | 模块 | 模块 | opt-in / `mobile-apps` | – | +| Task(没有模块) | **进程** | **进程** | – | off / off;由桌面端应用目录启动 | – | +| AppCard | 模块 | 模块 | 模块 | opt-in / opt-in | 自己的内核连接 | -Shell 在运行时如何决定(`crates/shell/src/apps.rs`,`AppRegistry::hosting`):如果目标平台不能运行进程(在 wasm 和原生移动平台上 `host::processes_available()` 为 false),所有应用都作为模块;App Hub 和 Settings 始终是模块;其他情况下原生应用按清单条目托管,并且只有存在进程形态(可以从中构建它的源码检出,或同目录下的二进制)时才作为进程运行;没有进程形态时回退到进程内(`manifest_default`)。桌面端发布包尚未在 main 上([#257](https://github.com/OctoSense-org/OctoSense/pull/257),进行中,它取代已关闭的 #94)。按应用的覆盖设置(`~/.makepad/wm/apps.splash`、`--module `)可以在模块和进程之间切换一个应用。 +`AppRegistry::hosting`(`crates/shell/src/apps.rs`)在每次启动应用时做决定。在没有进程的构建中(原生移动端、wasm),每个链接的应用都是模块,App Hub 和 Settings 则始终是模块。其他情况下,链接的原生应用按它的条目托管(`manifest_default`),除非在 OctoSense 主目录下的 `wm/apps.splash` 中切换过,或用 `--module ` 指定。只有能从源码检出构建、或在 Shell 旁找到二进制时,它才作为进程运行。Task 没有模块,所以即使在没有 Vulkan 的 Linux 上也作为进程运行。 -**进程托管**使用 Makepad 窗口管理器的托管机制(`crates/shell/src/clients.rs`、`hub.rs`): +发布包只附带 `octosense` 和内核,所以在发布包中 Terminal 在进程内运行:它自己的 Agent 只有只读工具,系统 Agent 也拿不到 `terminal.run`;发布包中也没有 Task。**尚未实现:**在发布包中附带进程应用。Rinx 仍是模块;ADR 0004 §2 允许通过一次经审查的 `hosting` 修改把它改为进程。 -- Shell 以 `--stdin-loop` 启动应用子进程,放在独立的进程组中,并设置 `STUDIO_HOST=http://127.0.0.1:` 和其客户端 id。在源码检出中,它先从 OctoSense 工作区构建应用的二进制(release 构建,受 `Cargo.lock` 约束,在任何沙箱之外运行),再在应用的沙箱中直接启动这个二进制,从不经过 `cargo run`(`clients::launch_plan`,[#225](https://github.com/OctoSense-org/OctoSense/pull/225));安装版的 Shell 启动同目录下的二进制。 -- 子进程回连 Shell 的 **hub**:Shell 在回环地址 8765–8784 中第一个空闲端口上绑定的 HTTP/WebSocket 服务(`WmHub::start`),使用 Makepad 的 studio 协议(`AppToStudio` / `StudioToApp`)。hub 只接受出示了 Shell 写入该子进程 stdin 的本次启动密钥的连接,每次启动只接受一次,并拒绝浏览器来源([ADR 0004 §5](adr/0004-native-apps-hosting-and-peers.md#5-the-peer-link-for-process-hosted-native-apps))。 -- 在操作系统允许时,画面以零拷贝方式到达合成器(在 Makepad `platform/src/os` 中实现):macOS 上用 IOSurface,Windows 上用 D3D11 共享句柄,Linux 在 Vulkan 加 Wayland 下用 DMA_BUF;Linux 的 OpenGL 构建则每帧经 CPU 回读。 -- 进程应用崩溃只影响它自己:用户打开的窗口意外退出时,其磁贴保留,显示为已关闭并提供 Restart,同时发出 “App stopped”([#130](https://github.com/OctoSense-org/OctoSense/pull/130);`clients.rs` 中的 `ClientSlot::stops_in_place`)。预热实例、AI 面板、预览或正在关闭的客户端照旧消失。 +**进程托管**(`crates/shell/src/clients.rs`、`hub.rs`)。在源码检出中,Shell 先从工作区构建应用,按其 `Cargo.lock` 锁定版本,且不在任何沙箱中;然后它自己在应用的沙箱中启动二进制,带上 `--stdin-loop`;已安装的 Shell 则启动自身旁边的二进制。子进程从 stdin 读取一次性密钥,凭它接入 Shell 的 hub:一个回环 WebSocket,端口是 8765–8784 中第一个空闲的端口。不在 `native-apps.json` 中的应用目录应用(Browser、Files 和其他上游 Makepad 应用)没有沙箱。进程应用意外退出时,它的磁贴保留,显示为已关闭并提供“重启”(`ClientSlot::stops_in_place`)。 -**进程内托管**(`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。** +**进程内托管**(`crates/shell/src/module_host.rs`)为每个模块实例提供独立的 Splash 隔离环境和存储命名空间,但模块的 Rust 代码与 Shell 共享内存。对模块的每次调用都在 `catch_unwind` 下运行(`contain`)。panic 会使该实例失败,把它进行中的工具调用答复为“结果未知”(只读调用除外),并显示“重启”。正在展开时再次 panic、`panic = "abort"` 构建以及 FFI 不在保护范围内。 ### 脚本应用 -脚本应用(系统应用 News、Photos、Maps、Mail、Calendar、AI providers 和 YouTube,加上仅手机上的 Camera,来自 `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。** +系统应用和商店应用运行在 App Hub 的 Card runner(`CARD_MODULE`)中:每个实例一个隔离环境,带文件 jail 和配额,只能通过 `host.request` 访问已授权的服务族。`desktop/system-apps.json` 和 `phone/system-apps.json` 列出各个 Shell 的系统应用。 ```mermaid flowchart TB subgraph desktop["桌面端(macOS)"] - ds["OctoSense 进程
Shell + App Hub + Card runner + Rinx"] + ds["OctoSense 进程
Shell + App Hub + Card runner + Rinx + Notes、Clock 等"] dk["octos 子进程
octos-kernel serve --stdio"] - dt["Terminal 子进程
--stdin-loop"] - ds -- "stdin/stdout" --> dk - dt -- "hub WebSocket、IOSurface 画面" --> ds + dt["Terminal 和 Task 子进程
--stdin-loop"] + ds -- "stdin/stdout:OUP" --> dk + dt -- "hub WebSocket:画面、AI 总线、peer link" --> ds end subgraph android["Android(Home)"] - as["Home 进程
Shell + App Hub + Card runner + Rinx(全部进程内)"] + as["Home 进程
Shell + App Hub + Card runner + Rinx + Notes、Clock 等(全部进程内)"] ak["liboctos.so 子进程
serve --stdio"] - as -- "stdin/stdout" --> ak + as -- "stdin/stdout:OUP" --> ak end subgraph ohos["OpenHarmony"] os["Home 进程
Shell + 内嵌 octos 任务"] @@ -170,142 +144,163 @@ flowchart TB ## 2. Agent -**每个 Shell 一个内核;Agent 是会话,不是进程。** 在这一个内核中,Agent 回合由 Tokio 任务执行,peer 与 session 是持久状态。一个 peer 可包含多个 context session,一个回合也可创建多个任务,不存在一个 Agent 对应一个任务的关系。实际线程与任务对应关系见[导读](architecture-walkthrough.zh-CN.md#10-映射到-rust-的实际执行模型)。文件编辑之类的工具在内核中运行;octos 的命令类工具会在会话的工作区中以短时子进程运行,但 OctoSense 不把它们提供给任何 Agent(见[第 4 节](#4-工具与授权))。 +每个 Agent 都是同一个内核中的会话,而不是进程。README 介绍了[这两类 Agent](../README.zh-CN.md#系统-agent-与应用-agent),并说明了它们[如何对应到线程](../README.zh-CN.md#为什么它省内存省算力)。 -| Agent | 是什么 | 状态 | -| --- | --- | --- | -| **系统 Agent** | `_main` profile 上的会话 `_main:api:octosense#system`(`crates/kernel/src/network.rs` 中的 `SYSTEM_SESSION`)。它拥有并监督所有应用 peer。用户在 Shell 的**系统对话**中与它对话(`crates/shell/src/system_chat/`,[#132](https://github.com/OctoSense-org/OctoSense/pull/132):Setup → Assistant → Assistant chat、F8、桌面 Dock 上的 Assistant 图标或手机主屏的 Assistant 磁贴;桌面上是左侧一个中等大小、用户可以移动和调整大小的面板,手机上为全屏),或通过已配对的 Talk to Octos 客户端 | 已在 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))。用户可在应用自己的界面中,或在 Shell 的 **“Ask ” 面板**(`crates/shell/src/app_chat/`:顶栏的 “Ask ”、Shift+F8;桌面上位于系统对话旁边,手机上为全屏面板)与它对话。面板显示两条通道及其发言者,以及一个输入框:用户的通道空闲时可以发送(即使系统 Agent 正在工作),用户自己的回合运行时可以停止,系统 Agent 正在运行的那一行上有 “Stop the system agent's task” | 带 Agent 的原生应用(Rinx、Terminal、Calculator、Clock、Notes、Reminders、Weather)和每个带 Agent 的脚本应用(目前是系统应用 News、Mail、Photos、Maps、YouTube、仅桌面端的 Calendar 和仅手机上的 Camera)已在 main,需首次使用同意;见下文 | +### 系统 Agent + +系统 Agent 是会话 `_main:api:octosense#system`(`crates/kernel/src/network.rs` 中的 `SYSTEM_SESSION`)。用户在系统对话(`crates/shell/src/system_chat/`)中与它交谈;系统对话运行在自己的线程上,只在面板打开或有回合运行时才保持连接。 + +它的内核工具恰好是 `SYSTEM_AGENT_TOOLS`(`crates/kernel/src/system_tools.rs`):四个 `peer_*` 工具、自己工作区里的文件、`ask_user_question` 和媒体查看、记忆、`web_search`、`web_fetch` 以及 `tool_search`。它永远拿不到 `peer_handoff`、`peer_close` 或 octos 自带的 shell(见[如何强制执行](#系统-agent-的工具集))。 + +系统对话在这个会话上注册宿主工具(`system_chat/session.rs` 中的 `ShellSystemHost::declarations`): -每个应用 peer 都独立拥有: +- `agents.list` 和 `agents.ask`(在 `crates/shell/src/agents.rs` 中声明):始终提供。 +- `agents.provision` 和 `agents.status`(同样在 `agents.rs` 中):只供邮件使用,且只在带 App Hub 或原生移动端的构建中(见[邮件事件](mail-agent-events.zh-CN.md))。 +- `terminal.run`:Setup 中的 Command execution 打开、且 Terminal 作为沙箱进程运行时(`system_chat/grants.rs`,`terminal_target`)。 +- 各原生应用的 `agent.system_tools`:本构建链接了该应用,或能把它作为进程启动时。应用没打开时,调用会回答 “Open <App> first”。 -- **工作区**:peer 会话所绑定的目录。自锁定 octos 665209e5 起新建的 peer 以账号目录(`apps//accounts//`,ADR 0004 §11)作为 `peer/prepare` 的 `cwd`(`host_tools::agent_workspace`),并记录在其宿主令牌旁边,恢复时使用同一目录;之前创建的 peer 保留内核分配的目录,因为 octos 不允许在另一个工作区下恢复 peer(见[不一致之处](#代码与-adr-不一致之处)); -- **记忆命名空间** `app//acct-`(`crates/app-peers/src/broker.rs` 中的 `app_namespace()`);不返回它的内核会被拒绝; -- 自己的**对话记录**和**模型**通道(宿主通过 `peer/prepare` / `peer/model/set` 设置模型); -- 自己的**工具列表**:内核工具恰好是其清单授予的那些(`generic_tools`,从不包括 octos 的 `shell`),加上 Shell 注册的工具(octos [#2567](https://github.com/octos-org/octos/pull/2567),UPCR-2026-035):每次 `peer/prepare` 和重连之后,broker 在驱动该 peer 回合的连接上注册应用声明的工具,以及授予它的其他应用可共享工具,每个都标明所属应用(`crates/shell/src/host_tools/`),并把 `generic_tools` 设为恰好是应用清单声明且用户授予的内核工具(原生应用为 `native-apps.json` 的 `agent.generic_tools`,脚本应用为其清单 `agent.tools` 中不带点的名称;一个也没写的应用就一个也没有)。Terminal 声明 `terminal.run`、`terminal.read_screen` 和 `terminal.read_scrollback`。系统应用在 `apps//bundle/tools.json` 中声明自己的工具,各自在一个宿主服务上运行:News 的 `news.list`、`news.read` 和 `news.notify`(`news` 服务),Mail 的 `mail.notify`(`mail` 服务),Calendar 的 `calendar.events`、`calendar.add_event`、`calendar.remove_event`、`calendar.notify` 和 `calendar.agenda`(`calendar` 服务),以及 Photos、Maps、YouTube 和 Camera 的 `.notify`(Shell 的通知服务,`crates/shell/src/glance_notice.rs`,因为它们没有自己的服务)。注册失败的 peer 不运行任何回合; -- **请求上下文**(`peer/context/open`):每个客户端实例一个(一个 Rinx 小程序),各有自己的对话记录、peer 工作区内的目录 `contexts//` 和子记忆命名空间。这类客户端上下文不能直接读旁边的文件;它通过 Shell 的宿主读取工具 `files.list`、`files.read` 和 `files.search`(ADR 0004 §11,`crates/shell/src/host_tools/files.rs`)读取账户数据,这些工具从不显示其他上下文的目录。除了这些按客户端划分的上下文(`OctosAppService::open_context`:Rinx 小程序,以及带 `client` 的进程应用 `octos.session.open`),每个应用对话也是一个请求上下文:用户与应用 Agent 的对话(来自 Shell 的 “Ask ” 面板、应用界面或卡片的卡内对话)在用户通道中运行,这是一个以 `share_history` 打开的上下文,与 peer 自己的会话(系统 Agent 的通道)并行,并让每条通道看到另一条通道最近的回合(ADR 0004 §6,2026-09-29 决定;见下文)。在清单声明 Agent 工作在账号文件夹中时(`storage.agent_workspace: "account"`,octos#2647),该对话上下文以 octos 的 `read_parent` 打开:它只读地读取账号文件夹,从不读取其他上下文的文件夹,且只写入自己的文件夹。 +octos 只列出已准备好的 peer,所以其余情况由 Shell 告诉系统 Agent:应用的 Agent 一有变化,就在它的回合里附上一段说明;`agents.list` 给出每个应用的 Agent、它的状态和 peer slug。`agents.ask` 弹出首次使用面板,并一直挂起这次调用,直到用户作答;对脚本应用,还要等到 peer 准备好。系统 Agent 从不批准任何东西;`peer_respond` 只回答问题。 -目前谁有 peer(`crates/ai-host/src/lib.rs`,`Policy::shipped()`;`crates/app-peers/src/hosted.rs`,`effective_services` = 声明 ∩ 支持 ∩ 策略): +### 应用 Agent -- `native-apps.json` 条目授予了 `agent.octos`(四个 `octos.*` 服务,生成到 `crates/ai-host/src/native_agents.rs`)的**原生应用**:Rinx、Terminal、Calculator、Clock、Notes、Reminders 和 Weather,需首次使用时的同意。原生应用的 peer 属于它已打开的实例,所以只在应用打开期间存在(`agents::prepare` 跳过原生应用)。 -- **带 Agent 的脚本应用**:清单声明了 `octos.*` 或 `agent` 块,或应用包带有 `tools.json`(`crates/shell/src/apps.rs`,`agent_apps`)。目前就是除 AI providers 之外的每个系统应用:News、Mail、Photos、Maps、YouTube、仅桌面端的 Calendar 和仅手机上的 Camera,每个都有 `agent` 块和 `tools.json`,都没有声明 `octos.*`。每个应用/账号一个 peer,broker 身份为 `card.`;无账号的应用使用 `device`,否则使用宿主报告的当前账号(Mail 需要先登录)(`crates/ai-host/src/contained.rs`,[#106](https://github.com/OctoSense-org/OctoSense/pull/106)),在用户首次使用同意后可用(发布时的闸门;`OCTOSENSE_CONTAINED_APPS=1` 不询问任何人,`0` 关闭)。用户一旦同意,以及每次启动时,Shell 就会准备其 peer(`crates/shell/src/agents.rs`,`contained::prepare`),因此即使应用从不调用 `octos`,系统 Agent 的 `peer_list` 也能看到它;应用自己的 `host.request("octos.*")` 仍限于其清单声明的服务。 -- **AppCard**(可选)使用自己的内核连接和会话,而不是 peer。 -- 其 `native-apps.json` 条目授予了 `agent.octos` 服务的**进程应用**,经 **peer link**(`crates/shell/src/peer_link/`,[#130](https://github.com/OctoSense-org/OctoSense/pull/130))访问自己的 peer,需首次使用同意。Terminal 就是这样:它用 Makepad 的 `OctosPeer` 打开自己 Agent 的链接,并在上面应答自己的只读工具。 +应用 Agent 是宿主拥有的 octos peer,对应一个(应用,账号),归系统 Agent 的会话所有(octos UPCR-2026-034),由一个代理驱动(`crates/app-peers/src/broker.rs`)。它的记忆命名空间是 `app//acct-`,内核若不确认这个命名空间,代理就拒绝该内核。它的工作区是账号文件夹(见[第 6 节](#6-存储与机密)),宿主 token 保存在 `/../app-peers` 下的一条记录中,在 Unix 上仅所有者可读写。每次 `peer/prepare` 和重新连接之后,代理都会注册它的工具;注册失败的 peer 不运行任何回合。 -peer 会被保留而不是丢弃:broker 用其宿主 token(存放在 `/../app-peers`,0600)恢复 peer,登出时也从不调用 `peer_close`,因为 octos 无法恢复已关闭的 peer,也无法为同一个(应用,账号)创建替代者(ADR 0004 §11)。 +脚本应用的 peer 名为 `card.`(`crates/ai-host/src/contained.rs`)。不区分账号的应用以 `device` 身份行事;邮件以最近登录的账号行事。哪些应用有 Agent 由 `apps::agent_apps` 决定;没有任何授权的应用得不到代理。 + +- 脚本应用的 peer 在 Agent 获准时准备好,此后每次启动时(`agents::start`)也会准备,所以即使应用关着,`peer_list` 也能看到它。 +- 原生应用的 peer 属于它已打开的实例。同时打开多个实例时,最早的那个驱动 peer,它关闭后由下一个接管(`driver_of`、`take_over`)。 +- 退出登录会挂起该账号:它的上下文关闭,工具调用得到 `signed_out`,也不会启动 `peer/input` 回合。重新登录会恢复同一个 peer。 + +### 两条通道 + +系统 Agent 的通道是 peer 自己的会话。用户的通道是一个以 `share_history` 打开的请求上下文(`open_conversation`),每个句柄新开一个;用户的回合(来源 `person`)和应用自己发起的回合(来源 `app`)都在这里运行(见[一个应用 Agent,两条通道](../README.zh-CN.md#一个应用-agent两条通道))。每个回合开始时,都会以只读方式看到另一条通道最近的文字:默认最近 20 条,总长不超过 16 KiB,另加对方正在运行的回合。 + +回合由什么触发,决定了审批如何对待它。“Ask” 面板发送 `ContextOp::TurnFrom { trigger: TurnTrigger::Person }`。不带触发来源的 `ContextOp::Turn` 算作 `Unknown`,信任度最低;脚本声称的 `trigger: "person"` 会变成 `AppSaysPerson`,中转把它当作应用自己的运行(`trigger_of`)。普通请求上下文(`open_context`,用于 Rinx 小程序)不共享历史,并被限制在 `contexts//` 中。 + +### “Ask <app>” 面板 + +Shell 为每个有 Agent 的应用绘制用户的通道(`crates/shell/src/app_chat/`),用的是系统对话的面板:桌面端在系统对话旁边,手机上全屏显示。 + +- **打开**需要 Agent 已获准(首次使用面板会先询问);对原生应用,还需要应用已打开,因为它的 peer 属于它的窗口。 +- **发送**开始一个用户回合,即使系统 Agent 的通道正在运行也可以;如果应用的 Agent 有未回答的问题,输入的文字就是回答。 +- **停止**只结束用户的回合;“Stop the system agent's task” 结束另一条通道的回合。审批面板或问题卡片上的停止,以及在应用对话上调用 `octos.turn.interrupt`,会同时结束两条通道的回合。 +- **问题**:来自应用对话的问题在面板打开时显示在面板中,否则显示在 Shell 的问题卡片上。 +- **关闭**只隐藏面板,保留它的上下文,直到面板为另一个应用打开、Agent 被关闭,或 peer 消失。 ### 内核崩溃意味着什么 -所有 Agent 都在同一个内核中,因此内核崩溃会同时停止**全部** Agent:系统 Agent 和每个应用 peer,包括正在进行的回合。Shell 和应用继续运行;`availability()` 报告助手失败,应用的常规界面照常工作,进行中的回合随连接一起结束。octos 已持久化的内容不会丢失:会话、黑板、记忆命名空间和 peer 绑定都在内核的数据目录中,因此下一次 `connect()` 启动新一代内核,使用方用保存的宿主 token 重新打开并恢复各自的 peer 和上下文。结果未知的工具调用未经用户同意绝不重试(ADR 0004 §7)。 +所有 Agent 都在同一个内核里,所以一次崩溃会让它们全部在回合中途停下。Shell 和应用继续运行,应用看到助手不可用。断开的连接上的工具调用直接结束,不会执行。会话、黑板、记忆和 peer 绑定都保存在磁盘上,所以下一次 `connect()` 之后,代理会用保存的宿主 token 恢复各自的 peer。 ## 3. 通信 ```mermaid flowchart LR subgraph shell["Shell(宿主连接)"] - broker["app-peers broker"] - relay["宿主工具中继
(crates/shell/src/host_tools)"] + broker["app-peers 代理
每个应用实例一个"] + relay["宿主工具中转
crates/shell/src/host_tools"] + router["审批路由"] end subgraph kernel["octos 内核"] sys["系统 Agent"] - p1["Calendar peer"] - p2["Mail peer"] + p1["日历的 peer"] + p2["邮件的 peer"] bb[("黑板
peers/<slug>/result.md、turns.txt")] end - rinx["Rinx(模块)"] -- "OctosAppService" --> broker + rinx["Rinx(模块)"] -- "注入的 OctosAppService" --> broker + mods["Calculator、Notes 等(模块)"] -- "peer link,在内存中" --> broker + proc["Terminal(进程)"] -- "peer link,走 hub 连接" --> broker card["脚本应用"] -- "host.request(octos.*)" --> broker - proc["进程应用"] -- "peer link(Terminal)" --> broker broker -- "OUP:peer/prepare、peer/context/open、turn/start" --> kernel sys -- "peer_send_input" --> p1 + kernel -- "peer/input、peer/tool/call、审批" --> broker + broker -- "工具调用、审批" --> relay + relay --> router p1 -- "写入" --> bb - sys -- "peer_gather / peer_list" --> bb - kernel -- "peer/input、peer/tool/call、host_tool 审批" --> relay + sys -- "peer_gather、peer_list" --> bb ``` ### 内核与客户端之间的 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 的前两行,从不放入环境变量: +OUP 是 JSON-RPC 2.0,`octos-ui/v1alpha1`(octos [`api/OCTOS_UI_PROTOCOL_V1_SPEC_2026-04-24.md`](https://github.com/octos-org/octos/blob/056173e85b150e387805fc307fe231064ac1ed35/api/OCTOS_UI_PROTOCOL_V1_SPEC_2026-04-24.md))。默认情况下,它在子进程的 stdin 和 stdout 上每行一帧,Shell 中所有使用方共用这条流。`crates/kernel/src/router.rs` 为每个请求分配内核内唯一的 id,把每个回复只交给发出请求的使用方,并把每条通知发给提到过该会话的使用方。 - | Token | 持有者 | 可以做什么 | - | --- | --- | --- | - | **宿主 token** | 只有 Shell 进程;每个 Shell 生命周期生成一次,从不记录日志 | 一切(管理员);原生使用方用它连接 `/api/ui-protocol/ws` | - | **外部 token** | 已配对的网页客户端(可信面板上的 8 位配对码,5 分钟有效,只能领取一次)或本用户的终端客户端(0600 的 `client-connection.json`) | 只能访问 `/api/ui-protocol/ws`,身份为 `_main`,且只能调用允许列表中的方法:打开并读取系统对话,启动、引导或中断**自己的**回合,回答自己回合的审批(一次性)和问题 | +**Talk to Octos**([ADR 0003](adr/0003-shared-octos-client-access.md),英文;octos [`docs/HOST_MANAGED_SERVE.md`](https://github.com/octos-org/octos/blob/056173e85b150e387805fc307fe231064ac1ed35/docs/HOST_MANAGED_SERVE.md))在桌面端和 Android 上把内核重启为 `serve --host 127.0.0.1 --host-managed`;在 Unix 上,Shell 把一个只绑定一次的监听套接字传给它。两个 token 都通过内核的 stdin 传入,从不放进环境变量。 - 外部客户端不能:调用任何 `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 未验证。 +宿主 token 留在 Shell 中,可以访问一切。外部 token 交给已配对的网页客户端(凭一次性配对码),或同一用户的终端客户端(`client-connection.json`,在 Unix 上仅所有者可读写)。它只能打开 `/api/ui-protocol/ws`,以 `_main` 身份访问系统对话和客户端自己的回合:没有 `peer/*` 方法,碰不到应用 peer,只有 octos 固定的 `EXTERNAL_TURN_TOOLS`。 ### 内核中的系统 Agent 与应用 Agent -使用 octos 的 peer 机制,深度为 1(peer 不能创建、引导或关闭 peer)。octos 中**已在 main**;这些工具位于 octos `crates/octos-agent/src/tools/`。 +octos 的 peer 工具([`crates/octos-agent/src/tools/`](https://github.com/octos-org/octos/tree/056173e85b150e387805fc307fe231064ac1ed35/crates/octos-agent/src/tools))只有一层:peer 不能创建、操纵或关闭另一个 peer。 -- **系统 Agent → 应用 Agent。** `peer_send_input`(只有发起者可用,最多 64 KB)经 peer 的收件箱把文字作为 peer 的下一个用户回合送达(在 serve 中是持久队列,每隔几秒处理一次,至少送达一次)。 -- **系统 Agent 能联系到哪些应用的 Agent。** `peer_list` 列出已准备好的 peer:Shell 会准备每个已允许应用的 Agent。对它列不出来的(尚未允许或已关闭),Shell 会告诉它:应用的 Agent 有变化时,在系统对话的回合中附上一条说明;以及系统会话上的宿主工具 `agents.list` 和 `agents.ask`(`crates/shell/src/agents.rs`)。`agents.ask` 显示首次使用面板,只有用户能回答,并且它会等用户作答:它被声明为 `risk: act`、`outward: true`、`confirm: "app"`,所以内核像持有审批一样持有这次调用;用户作答且该应用的 peer 就绪后,系统对话带着 peer 的 slug 回答它,系统 Agent 就能在同一个回合里继续处理请求(10 分钟仍无回答时,它回答用户还没有作答,面板保持显示;[#274](https://github.com/OctoSense-org/OctoSense/pull/274))。 -- **应用 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 会拒绝)。octos 在宿主驱动的回合中保留 `ask_user_question`(`generic_tools` 是宿主设定的精确列表,省略时保留全部工具)。ADR 0004 §6(2026-09-29 决定)由 Shell 把每个问题转到正确的对话:应用自己的对话,或者对系统 Agent 的回合(包括 `peer/input` 回合)转到系统对话;没有 `host.ask`。**已在 main**:系统对话回答系统 Agent 自己会话上的问题;broker 把应用 peer 的 `user_question/requested`(其自身会话、请求上下文或 `peer/input` 回合)连同回合的发起者(octos 报告时用它,否则推断:broker 为 `peer/input` 发起的回合属于系统 Agent)交给 Shell 的请求模型(`crates/shell/src/questions/`),请求模型按发起者路由:用户或应用的回合转到应用的对话(该应用的 “Ask ” 面板打开时在面板中,否则在 Shell 审批浮层的问题卡片中,`crates/shell/src/approvals/view.rs`:同一时间只有一个界面,[#279](https://github.com/OctoSense-org/OctoSense/pull/279)),系统 Agent 的回合转到系统对话。只有用户能在 Shell 界面上回答(在 broker 的连接上发送 `user_question/respond`);应用的上下文只收到 `user_question/handled_by_host`,broker 拒绝应用回答宿主持有的 id。回合结束时问题关闭。 -- **每个应用 Agent 两条并行通道,只读共享历史。** ADR 0004 §6(2026-09-29 决定,取代同日 [#166](https://github.com/OctoSense-org/OctoSense/pull/166) 与 octos [#2626](https://github.com/octos-org/octos/pull/2626) 的单一排队对话):系统 Agent 与 peer 自己的会话对话(`peer/input`),用户(应用界面、其卡片、不带 `client` 的进程应用)在 broker 以 `share_history` 打开的请求上下文中对话(octos [#2636](https://github.com/octos-org/octos/pull/2636),UPCR-2026-034),每个句柄一个新的上下文 id(其历史也包含同一账号、同一实例之前各个句柄中用户的消息)。两条通道并行运行:用户的消息只等自己上下文的上一轮,`peer/input` 在 peer 会话上保留自己的队列。每一轮开始时,内核把另一条通道最近的用户/助手文本(默认 20 条,最多 50 条,16 KiB 以内;去掉工具行)作为只读块给模型看,这个块从不写入读取方的对话记录;没有共享的对话记录。每一轮都保留 `origin`(`person`、`system_agent`、`app`;内核的 `[from …]` 标记;宿主不能改写系统 Agent 回合的标记),用户的回合在黑板上留下一轮结果(`origin: person`、`context: `),系统 Agent 用 `peer_gather` 读取,它本身不会触发 fleet 汇总。`OctosAppService::open_conversation` 是用户的通道;`open_context(client)` 仍是普通请求上下文(Rinx 小程序)。应用的跟随者(`OctosContext::subscribe`;peer link 上的 `conversation` 帧)收到两条通道的事件,每个都带 `lane` 与 `speaker`;`octos.session.history` 按 `persisted_at` 合并两份对话记录,每行带 `lane`,带标记的用户消息另附 `speaker` 与 `display_text`。接受的风险:跨通道的文件编辑不加锁(每条通道编辑自己的文件夹,上下文的是 `contexts/`),未知结果标记是共享的,并行回合可能略微超出 peer 的 token 预算。 +- **发给应用 Agent:**`peer_send_input`,最多 64 KiB,只有 peer 的发起者能发。octos 把这一轮交给 Shell(见[下文](#shell-如何运行系统-agent-的输入))。 +- **回传:**黑板,peer 之间唯一的通道。每个 peer 回合都会写入 `peers//result.md`,并在 `turns.txt` 中加一行,系统 Agent 用 `peer_gather` 和 `peer_list` 读取。 +- **提问:**代理把每个 `ask_user_question` 交给 `crates/shell/src/questions/`,由它按回合的来源分发(见[提问](../README.zh-CN.md#提问))。 -### 宿主拥有的路径:`peer/input` +### Shell 如何运行系统 Agent 的输入 -对宿主拥有的 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 该应用未连接。**已在 main**:自锁定 665209e5 起 octos 会发送它(octos [#2567](https://github.com/octos-org/octos/pull/2567));broker(`crates/app-peers/src/broker.rs`)用内核的 `turn_id` 在自己的连接上启动回合,丢弃重复的 `input_id`,对已登出的账号或用户未允许的应用不启动任何回合(`host_tools::ShellToolHost::admit_input`),而是用 `peer/input/reject` 连同原因拒绝它(octos [#2621](https://github.com/octos-org/octos/pull/2621));启动失败的回合也以同样方式拒绝。同一个应用在内核上有多个实例时,该账号最早仍在运行的实例驱动这个 peer(注册其工具、接收其 `peer/input`),它关闭时由下一个实例接管([#201](https://github.com/OctoSense-org/OctoSense/pull/201))。内核每个会话只接受一个回合,且不排队(`turn_in_progress`),所以 broker 为系统 Agent 在 peer 会话上的输入每个 peer 维护一个队列,一次一个回合(被以 `turn_in_progress` 拒绝的启动会用同一个回合 id 重试);已有 8 个在等待时,新的输入以 `busy` 被拒绝。用户的消息在自己的通道里运行,从不在这个队列里等待。任何通道都不会被长时间占住:peer 会话或上下文上的审批或问题 10 分钟后过期(`OCTOSENSE_PROMPT_DEADLINE_SECS`;审批路由拒绝该审批,请求模型以自由文本婉拒该问题,从不批准,两者都显示 “Expired: no answer in 10 min”,直到被关闭),30 秒后仍在运行的回合会被 broker 中断,以便队列中的下一个回合开始。用户的 Stop 结束两条通道上正在运行的回合,用户的和系统 Agent 的:对话上的 `octos.turn.interrupt`(应用、卡片、peer link),以及 Shell 审批面板和问题卡片上的 “Stop 's agent”(`approvals::stop_agent`、`broker::interrupt_where`)。“Ask ” 面板一次只停一条通道(`broker::interrupt_lane_where`):它的 Stop 结束用户自己的回合,“Stop the system agent's task” 结束系统 Agent 的回合。Shell 的各个内核使用方共用一个内核连接(`crates/kernel/src/router.rs`),因此每个 broker 和系统对话对 octos 来说是同一个宿主连接;各自只回答自己的 peer 或会话的调用。 +octos 把系统 Agent 的输入以 `peer/input {peer, session_id, input_id, turn_id, text}` 的形式送到驱动该 peer 的连接上。代理用内核给出的回合 id 自己启动这一轮,所以这一轮带着应用的工具、记忆和审批(见[系统 Agent 如何与应用 Agent 通信](../README.zh-CN.md#系统-agent-如何与应用-agent-通信))。 + +如果 Agent 未获准、账号已退出登录、工作区被拒绝,或这一轮启动失败,代理会回复 `peer/input/reject` 并说明原因(`ShellToolHost::admit_input`)。内核每个会话同时只运行一轮,也不排队,所以代理为每个 peer 维护一个系统 Agent 输入队列,逐个启动。已有 8 个在等待时,新的输入会以 `busy` 被拒绝。用户的通道从不在这个队列里等待。 ### 应用与它自己的 Agent -| 托管方式 | 通道 | 状态 | +应用从不直接使用 OUP,也看不到宿主 token;Shell 会在每次调用上标注应用的身份(见[应用如何使用自己的 Agent](../README.zh-CN.md#应用如何使用自己的-agent))。共有三条路径: + +| 路径 | 使用者 | 工作方式 | | --- | --- | --- | -| 进程内原生模块,经 peer link | Makepad 的 `OctosPeer::open` 暂存一对通道;模块宿主把它归给打开它的实例,Shell 用进程应用同一套 **peer link** 服务它(`peer_link::module_connected`、`octosense_ai_host::module_peer`),应用无需知道自己如何被承载 | 已在 main([#142](https://github.com/OctoSense-org/OctoSense/issues/142));Calculator、Clock、Notes、Reminders 和 Weather 在用,Terminal 在进程内运行时也在用 | -| 进程内原生模块(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_gate`(默认 `ContainedGate::Consent`)和首次使用同意约束;其 peer 发起的工具审批交给 Shell 的审批路由。系统应用都不调用 `octos.*`,也都不绘制对话界面:用户在 Shell 的 “Ask ” 面板中与它们的 Agent 对话,用的是同一个 peer,Shell 也为系统 Agent 驱动它们 | 已在 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` 中 | Shell 一侧已在 main([#130](https://github.com/OctoSense-org/OctoSense/pull/130),`crates/shell/src/peer_link/`,帧由 `lib.rs` 从 hub 连接转发);Terminal 在用 | +| Peer link(`crates/shell/src/peer_link/`) | Calculator、Clock、Notes、Reminders、Weather、Terminal | Makepad 的 `OctosPeer` 客户端。进程应用的链接走它的 hub 连接。模块的 `OctosPeer::open` 先暂存一条内存中的通道,`module_host` 把它认领给打开它的实例,Shell 再以同样的帧提供服务(`peer_link::module_connected`)。`serve_tools` 应答 Agent 的工具调用。 | +| 注入的服务 | Rinx | 在模块 `create` 之前调用 `ai_host::offer`,在 `create` 中调用 `injection::claim`,实例由此得到一个限定范围的 `OctosAppService`(`open_conversation`、`open_context`)。 | +| `host.request("octos.*")` | 商店应用 | Card runner 的 `octos` 宿主服务(`crates/ai-host/src/contained.rs`):只限 manifest 声明的服务,并需首次使用同意。 | -### Makepad 的 AI 服务总线与 OctoSense 的应用 Agent +在 peer link 上,身份就是那个套接字或模块实例;一次调用的账号、上下文和客户端,来自 Shell 对该应用所开上下文的记录。进程退出时,它未完成的调用失败(除只读调用外均为 `outcome_unknown`),peer 保留。 -两种模型并存(ADR 0004,“Two AI models”): +Rinx 在其锁定的版本中,只把注入的服务用于小程序的上下文。用户通过 “Ask Rinx” 面板与它的 Agent 对话;Rinx 自己的助手工具在 AI 服务总线上,它的 Agent 不声明任何工具。脚本应用收不到推送事件:`octos.turn.start` 返回汇总后的回复,`octos.session.history` 返回按时间合并的两条通道(**尚未实现:**流式事件)。没有系统应用声明 `octos.*`;它们的 Agent 由 Shell 驱动。 -| | 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`;原生应用没有 peer link 时它自己的工具(`host_tools/relay.rs`,`serves_on_bus`)。其 `confirm: host` 调用经过 Shell 的审批路由([#120](https://github.com/OctoSense-org/OctoSense/pull/120)) | 应用自己的助手;系统 Agent 的委派 | +### AI 服务总线 -**为什么应用 Agent 的流量不走总线。** 总线是通向一个掌握全部上下文的中心 Agent 的窄而单向的 API;应用 Agent 需要应用的完整上下文和一个私有、受监督的会话,而系统 Agent 必须通过内核的 peer 机制与它通信。总线也不携带账号、请求上下文或调用方,无法按 ADR 0004 §5 的要求在每次工具调用上盖上身份,而且其注册对面板可见。因此在 OctoSense 中,总线不是系统 Agent 与应用 Agent 对话的通道;Shell 的中转只用它在原生应用已打开的实例中运行工具(Terminal 由宿主确认的 `run`,以及应用没有 peer link 时它自己的工具)。它留给上游 Makepad 应用(ADR 0004 §6),进程应用的 peer link 也有意设计为独立通道。`crates/ai-host` 和 `crates/app-peers` 都不使用总线。 +总线是 Makepad 的另一种 AI 模型:一个中心对话,即桌面端的 AI 面板(`aichat`),调用应用按风险级别注册的类型化工具。Shell 这一半(`crates/shell/src/ai_bus.rs`)给每个应用的帧标上它的端点,把注册转发给面板,路由面板的调用,自己应答 `os` 服务,并把 `confirm: host` 的调用留给审批路由决定。OctoSense 用它承载 AI 面板和 Rinx 的助手工具;当原生应用既没有执行器也没有 peer link 时,中转也经它调用该应用的工具;`terminal.run` 始终走总线。应用 Agent 不用总线:它不携带账号、上下文或调用方。 ## 4. 工具与授权 -**清单声明,用户在安装时授权,Shell 在每次调用时执行检查**(ADR 0004 §12,ADR 0002 §4)。脚本应用在 `manifest.json` 中声明能力,在 `tools.json` 中声明工具(由 App Hub 准入并锁定);原生应用在经过评审的 `native-apps.json` 条目中声明(`agent.octos`、`agent.tools`、`agent.tool_policy`)。 +manifest 声明,用户在安装时授权,Shell 在每次调用时强制执行(ADR 0004 §12)。脚本应用在 `tools.json` 中声明工具;原生应用在 `native-apps.json` 的条目中声明它的 Agent(`agent.octos`、`tools`、`own_tools`、`system_tools`、`grants`、`generic_tools`、`budget`、`tool_policy`)。其中大部分字段的含义见 README 的[应用要给 Agent 提供什么](../README.zh-CN.md#应用要给-agent-提供什么)。`tool_policy` 设定工具由谁确认:Terminal 的 `run` 是 `confirm: host` 且 `auto_approvable: false`,所以任何常设规则都不会替用户批准它。 -Agent 的工具来源: - -| 来源 | 示例 | 在哪里运行 | 状态 | +| 来源 | 声明于 | 运行在 | 现状 | | --- | --- | --- | --- | -| 应用自己的工具(`tools.json`:名称 `.`、schema、`risk`、`confirm: host` 或 `app`、`shareable`) | `news.list`、`terminal.read_screen`、`mail.notify`、`calendar.add_event` | 应用的宿主服务、模块或进程(或 Shell 的通知服务),由 Shell 调用 | 已在 main(`crates/shell/src/host_tools/`):原生应用从 `native-apps.json` 的 `agent.tools` 声明,脚本应用从准入后应用包的 `tools.json` 声明(`host_tools/script_apps.rs`,使用 App Hub 的加载器);按(所属应用,工具)和调用方授权,参数和结果按声明的 schema 检查,计入预算,盖上账号、上下文和客户端,并路由到进程应用的 peer link、进程内模块的执行器(`OctosAppService::set_tool_executor`)、脚本应用的宿主服务(`HostServiceExecutor`:工具命名空间对应的服务,应用的清单必须获授权该服务,系统应用自己的命名空间除外,例如 Calendar 的 `calendar`)或 Terminal 的 AI 总线服务。News 提供 `news.list`、`news.read` 和 `news.notify`;Mail 提供账户绑定读取、草稿/提议工具、`mail.notify` 和 `mail.publish_card`(完整列表见 `apps/mail/bundle/tools.json`);Calendar 提供 `calendar.events`、`calendar.add_event`、`calendar.remove_event`(破坏性,`confirm: host`)、`calendar.notify` 和 `calendar.agenda`;Photos、Maps、YouTube 和 Camera 提供 `.notify`,由 Shell 的通知服务执行;Terminal 提供 `terminal.run` 及其读取工具 | -| 系统工具箱,按能力授予(`research`、`crawl`) | `toolbox.search`、`toolbox.web_read`、`toolbox.deep_crawl`、`workflow.run` | 宿主(`crates/toolbox`) | 已在 main,位于 `toolbox-peers` 特性之后([#151](https://github.com/OctoSense-org/OctoSense/pull/151);手机构建默认开启,桌面端默认关闭;`crates/ai-host/src/toolbox_peers.rs`):`research` 提供 `workflow.run`、`workflow.fork`、`toolbox.search` 和 `toolbox.web_read`,`crawl` 提供 `toolbox.deep_crawl`,只在用户同意后提供。在 App Hub 检查这些能力之前,脚本应用的声明只对系统应用有效,目前还没有应用声明其中任何一个 | -| octos 的通用工具 | 限定在工作区内的文件读取、记忆、`web_search`、`deep_search` | octos | 已在 main 上按应用设置:每个 peer 注册时带着它被授予的精确 `generic_tools`(Rinx:工作区文件、`ask_user_question`、记忆、网页);没有授予的应用一个也没有;octos 的 `shell` 从不在其中(生成器、目录和脚本加载器都会去掉它),`_main` profile 的 `tool_policy` 也拒绝它 | -| 其他应用可共享的工具,由 Shell 路由 | Calendar 的 Agent 调用 Mail 的 `mail.send`(规划中:Mail 还没有声明这个工具) | 所属应用,经 Shell | 已在 main(`relay::Catalog`):按(所属应用,工具)授予,原生应用来自 `native-apps.json` 的 `agent.grants`,脚本应用来自其 `agent.tools` 中带点的名称(安装时);注册时标明所属应用,每次调用都检查。News 的 `news.list` 和 `news.read` 是可共享的,但还没有应用申请其他应用的工具 | -| 命令执行 | `terminal.run`(Terminal 的可共享工具:`confirm: host`、`auto_approvable: false`) | 宿主工具,在用户可见的终端中 | 系统 Agent 已在 main:Setup › Assistant › Command execution 开启、且 Terminal 作为自己的沙箱进程运行时(它最近一次启动在其操作系统沙箱中运行:macOS,以及有 Vulkan 和 Wayland 的 Linux;Windows 不行,它的沙箱尚未实现),注册在其会话上,每次调用经路由作为命令实时批准,输入到正在运行的 Terminal。进程内 Terminal 只提供读取工具,并按 ADR 0004 §10(2026-09-29 决定)保持只读。授予应用 Agent 属于步骤 11 | +| 应用自己的工具,`.` | `tools.json`;`agent.tools` | 应用的宿主服务、通知服务或应用的窗口 | `own_tools` 把 Terminal 的工具限定为两个只读工具;邮件的 Agent 能起草和提议回复,但没有能发送的工具 | +| octos 的内核工具 | `agent.tools` 中不带点的名称;`agent.generic_tools` | octos | 脚本应用只有 `ask_user_question`;Rinx 有文件、记忆和网页工具;其他原生 Agent 没有 | +| `files.list`、`files.read`、`files.search` | Shell,注册在已获同意、有工作区的 peer 上(Unix) | Shell,作用于调用方的账号文件夹 | 每次读取最多 128 KiB,每次列出最多 500 项,每次搜索最多 100 个匹配 | +| 其他应用可共享的工具 | `agent.tools` 中带点的名称;`agent.grants` | 所属应用,经中转 | 新闻共享 `news.list` 和 `news.read`;还没有应用申请 | +| 工具箱(`toolbox.*`、`workflow.*`) | `research` 和 `crawl` 能力 | Shell([`crates/toolbox`](../crates/toolbox/README.md)),需 `toolbox-peers` feature,手机上默认开启 | 还没有应用声明 | +| `dev.run` | 开发者模式 | Shell(`host_tools/dev_run.rs`) | 仅限所覆盖的应用 | -**Agent 放到 Glance 屏幕上的内容。** `.notify` 填充宿主的固定通知模板(`crates/shell/resources/glance/notice.card`);Calendar 的 `notify` 与 `agenda` 填充其日程/议程模板,这些工具由模型提供文字。Mail 另有 `mail.publish_card`,接收模型编写的 L0 源码,并可附加宿主签发的草稿绑定。发布仍要求调用应用获得 `glance` 准入;重复卡片 ID 替换该应用的原发布。 +应用的 `AGENT.md` 和技能不是工具:代理把它们的文字作为宿主指导随每一轮发送(`crates/app-peers/src/guidance.rs`)。 -手机通知在当前屏幕上方打开准确对应的卡片工作区;过期卡片回退到 Glance。紧凑摘要不执行生成界面,点击后驻留工作区展开到根安全视口,无须启动另一个应用。有 Agent 的发布者即使在 L0 或 Splash 源码中没有 `sys.chat`,也能使用原生 Card / Chat;宿主把有大小限制的发布信息和 L0 状态绑定到原账户。Mail 专用 Email / Chat 读取同一持久草稿,发送另经宿主审核。显式 `sys.chat` 保留声明的线程;显式 Mail 演示仍使用固定回复。桌面 toast、Glance 面板及移除/撤销保留原有入口。代码和实机证据见[卡片工作区导读](mail-composable-cards.zh-CN.md#所有发布者共用的卡片工作区)。 +### 中转 -**系统 Agent 的工具集**(`crates/kernel/src/system_tools.rs`,[#117](https://github.com/OctoSense-org/OctoSense/pull/117))。**已在 main**,已生效: +中转(`crates/shell/src/host_tools/`)在 UI 线程上处理代理和系统对话送来的每一个 `peer/tool/call`: -- 默认列表 `SYSTEM_AGENT_TOOLS`:监督(`peer_send_input`、`peer_gather`、`peer_list`、`peer_respond`;不含 `peer_handoff`,也不含 `peer_close`,`_main` 配置文件的 `tool_policy` 对所有智能体禁用它)、其工作区的文件工具、`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 持久的宿主专用 `session/tool_list/set`(octos#2648)把系统会话的内核工具设为 `SystemAgentTools::kernel_tools`;授权变化时再设一次。该会话上的每个回合(无论由谁发起)都被收窄到它;宿主工具(`terminal.run`)在它之外注册,`spawn` 系列工具从不在其中。真实内核测试:`a_system_agent_turn_is_offered_exactly_the_system_agent_tools`。 -- **从 Settings 开启命令执行**(默认关闭,每条命令实时批准):Setup › Assistant › Command execution 设置授权;开启期间,系统对话在自己的连接上把 `terminal.run` 注册到系统会话(不带 `peer` 的 `peer/tools/register`),关闭时立即撤回。对话的连接就是宿主自己的连接,octos 对它不需要凭据(octos#2657,[#146](https://github.com/OctoSense-org/OctoSense/issues/146)):在任何应用的 Agent 启动之前就会立即提供该工具。只有在 Terminal 作为自己的进程运行、且它最近一次启动在其操作系统沙箱中运行时,这项授权才生效(ADR 0004 §10 和 §12,2026-09-29 决定;`system_chat::grants::terminal_target`):在没有 Vulkan 的 Linux、Windows(尚无沙箱)和手机上,`terminal.run` 不会注册,Setup 会说明它需要作为沙箱进程运行的 Terminal。 +1. **授权**,按(所属应用,工具)和调用方:应用自己的 Agent 可以调用自己的工具,其他应用的 Agent 只能调用授予它的工具(`Catalog::may_call`),系统 Agent 只能调用它的宿主工具。未获同意或账号已挂起时,调用会被拒绝。 +2. **检查**参数(最多 64 KiB)是否符合 `input_schema`,并从调用方的预算中扣除:除非 `agent.budget` 另有规定,每轮 32 次、每天 1000 次。 +3. **路由**到所属应用的执行器(`HostServiceExecutor`,或模块的 `set_tool_executor`);没有执行器就走它的 peer link,再没有就走它的 AI 总线服务。应用同时在总线上提供的 `confirm: host` 工具不走 peer link。 +4. **确认** `confirm: app` 调用:先向内核确认收到,再交给所属应用的面板(见[第 5 节](#5-审批))。 +5. **只回答一次**,结果要符合 `output_schema`(最多 256 KiB);取消之后什么都不再运行。每次调用都会连同参数摘要记录到 `logs/tool-calls.jsonl`。 -**外部客户端**只有 octos 的固定允许列表([第 3 节](#内核与客户端之间的-oup));宿主路由的工具、开发者授权和 `dev.run` 永远到不了它们。 +脚本应用中 `implemented_by: "host-service"` 的工具,以该应用的身份在其命名空间对应的宿主服务上运行,前提是应用获授了该服务族,或该服务族就是这个系统应用自己的。Shell 的 `NoticeService` 为相册、地图、YouTube 和相机应答 `.notify`。**尚未实现:**`implemented_by: "app"` 没有执行器,商店应用也没有宿主服务,所以商店应用的 Agent 还不能通过自己的工具做事。 -## 5. 审批 +### 系统 Agent 的工具集 + +每次启动内核之前,`enforce`(`crates/kernel/src/system_tools.rs`)都会写入 `_main` profile 的 `tool_policy`,对所有会话禁用 octos 的 shell(`group:runtime`)和 `peer_close`。它只替换 OctoSense 自己写的策略;写入失败时,内核不会启动。随后每次启动,都会用 octos 持久的、仅宿主可用的 `session/tool_list/set` 把系统会话的工具设为 `SYSTEM_AGENT_TOOLS`,这会限定该会话上的每一个回合;宿主工具注册在它旁边。应用 peer 只得到授予它的 `generic_tools`。 + +### Agent 往 glance 屏幕上放什么 -**授权不等于审批。** 授权表示 Agent 可以*拥有*某个工具;审批表示*这一次*调用、带着这些确切参数,可以执行。只读和应用内操作类工具授权后即可运行;对外或破坏性的调用(发送、发布、分享、购买、删除、运行命令)需要用户实时批准或由常设规则批准。只有用户能批准;系统 Agent 从不批准,Agent 自己输出的文字也从不作为审批界面(ADR 0004 §8)。 +glance 服务(`crates/shell/src/glance.rs`)以调用方应用的身份、在宿主记录的账号下发布每张卡片,而且只在应用有 `glance` 授权时才发布。一个应用每分钟最多发布 6 次,最多保留 4 张卡片;glance 屏幕显示服务保存的 32 张卡片中的 6 张。`mail.publish_card` 还能把卡片绑定到邮件已保存的某份草稿上;绑定后的卡片不能再换到别的账号、邮件或草稿。 + +在手机上,glance 列表只绘制紧凑的摘要,不运行任何生成的界面(`mobile_pages.rs`)。点按摘要会把它展开成常驻的全屏工作区(`glance_sheet.rs`),通知则直接打开它对应卡片的工作区;在桌面端,工作区居中打开。有 Agent 的发布者即使卡片没有声明 `sys.chat`,也会得到 Card / Chat 两个标签(`glance_card.rs` 中的 `WorkspaceChat`);邮件的回复卡片则在同一份已保存的草稿上提供 Email / Chat。细节见[组合式邮件卡片](mail-composable-cards.zh-CN.md#所有发布者共用的卡片工作区)。卡片模板见 README 的[系统 Agent 如何与应用 Agent 通信](../README.zh-CN.md#系统-agent-如何与应用-agent-通信),卡片自己的策略和卡内对话见[卡片与提问](../README.zh-CN.md#卡片与提问)。 + +## 5. 审批 -**Agent 工具调用与用户操作。** 审批路由处理 `peer/tool/call`;普通应用 UI 操作按发布者策略经过 Card runner 服务关口及宿主服务检查。Mail 发送还必须经过宿主拥有的确切邮件审核及 Android 实体批准,包括旧版编辑器的发送入口;生成按钮、开发者模式及常设规则都不能授权 SMTP。仿冒卡片不能回答 `approval/respond` 或获得 Mail 审核凭证。生成卡片在发布者策略下的隔离环境中运行。L0 操作由 `glance_card::LiveCards` 分发;手机上只在展开工作区中执行,收起的摘要不执行这些操作。 +授权让 Agent 拥有某个工具;审批让这一次、带着这些确切参数的调用得以运行。读取和应用内操作获授权后即可运行;破坏性或对外的调用则需要用户,要么当场确认,要么由常设规则批准。只有用户能批准(ADR 0004 §8)。审批路由只管 Agent 的工具调用:用户在应用或其卡片中所做的事,是应用自己的操作。 -**审批路由**(`crates/shell/src/approvals/router.rs`,[#120](https://github.com/OctoSense-org/OctoSense/pull/120))是 Shell 中唯一回答审批请求的地方,依据的是确切参数。**已在 main。** 对每个请求依次: +审批路由(`crates/shell/src/approvals/router.rs`)按以下顺序决定: ```mermaid flowchart TB @@ -334,109 +329,114 @@ flowchart TB sheet --> person ``` -0. **外部客户端的回合**不归 Shell 处理(octos#2624):路由不持有、不回答任何东西(`Route::LeftToClient`),只发出一条提示;由发起该回合的 Talk to Octos 客户端回答。 -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`,或在 Settings 中开启:桌面端在 Setup › Developer options 中输入确认短语,手机上在 Home 的 Settings › About phone 中连续点按 Build number 七次显示 Developer options,再在那里确认开启;开启范围是 Developer options › Apps it covers 中选定的应用(每个 home 保存在 `assistant/developer-apps.json`);商店构建永远不能。开启时显示横幅,审计每次调用,不在开发者 profile 中时 8 小时后或重启时自动结束。`dev.run` 作为被覆盖应用自己的宿主工具注册在它的 peer 上,由 shell 执行(`crates/shell/src/host_tools/dev_run.rs`),从不注册在系统智能体的会话上。 -2. **`confirm: app`** 工具交给所属应用自己的面板(应用通过 `register_app_confirm` 注册的 `AppConfirm`),并附上调用方;规则不回答它们。未注册面板的应用有 120 秒(`app_wait_s`),之后调用被明确拒绝。进程内应用通过自己的服务注册面板(`OctosAppService::set_confirm_sheet`,由 `host_tools::SheetBridge` 接入),进程应用的面板在其 peer link 打开时注册;所锁定标签的 Rinx 还没有这样做,因此它的发送面板仍在 Rinx 内部作答。main 上唯一的 `confirm: app` 工具是 Shell 自己的 `agents.ask`([第 3 节](#内核中的系统-agent-与应用-agent)),由系统对话自己持有。 -3. **`auto_approvable: false`**、**结果未知**以及不是经宿主自己的连接到来的调用,总是交给用户。 -4. **常设规则**(`rules.rs`),以**(所属应用,工具)**为键,不论谁调用;可以对确切参数设条件(收件人在联系人中或在该会话中、无附件、由用户触发、次数或金额上限;调用缺少所需信息时条件不成立),有每日上限(工具规则默认 20 次),范围最宽的规则(“这个应用接下来一小时的所有请求”)最多 60 分钟(没有“一切、永久”的规则),还有一个“关闭所有规则”。联系人来自 Mail 宿主服务的数据(用户自己的账户,以及用户发过邮件的地址;`contacts.rs`),并且只有在用户于设置中打开“在审批规则中使用我的联系人”(默认关闭)之后才会使用;在此之前“收件人在联系人中”从不匹配。系统通讯录是后续工作。由收到的内容触发的运行会被跳过,除非规则明确包含。用户在 Settings → Assistant → Approvals(`settings_page.rs`)中或从面板上创建规则;系统 Agent 只能建议。 -5. 否则显示 **Shell 绘制的面板**(`sheet.rs`、`view.rs`):每一行显示所属应用、工具、确切参数,跨应用调用时还显示调用方应用;系统 Agent 同一请求的 `confirm: host` 审批可以合并到它对话中的一个面板。 +0. **外部客户端的回合**交给该客户端;审批路由只发一条通知。 +1. **开发者模式**批准所覆盖应用的一切,包括 `auto_approvable: false` 和 `confirm: app`,但只在宿主自己的连接上。 +2. **`confirm: app`** 交给所属应用自己的面板,面板会显示调用方。没有注册面板的应用有 120 秒时间,之后调用被明确拒绝。 +3. **总是交给用户:**`auto_approvable: false` 的工具(例如 Terminal 的命令)、结果未知的调用,以及不在宿主连接上的调用。 +4. **常设规则**,按(所属应用,工具)匹配,不论调用方是谁。由收到的内容或未知来源触发的运行会跳过规则,除非某条规则明确纳入。 +5. 否则由 **Shell 绘制的面板**列出所属应用、工具、确切参数和调用方应用(如有)。系统 Agent 为同一请求发起的审批可以合并到一个面板中。 -每个决定都只向中继发送一次,并写入**审计**(`audit.rs`:`/logs/approvals-audit.jsonl` 中每个决定一行 JSON,仅所有者可读,记录参数的摘要而不是参数本身;开发者模式在 `logs/dev-audit.jsonl` 中另有完整审计);每个自动决定也会作为通知告知用户。规则存放在 `approvals/rules.json`,同意记录在 `approvals/consent.json`,都在 OctoSense 主目录下。 +**发送邮件**从不经过审批路由。邮件自己的写信界面(`mail.review_send`)和它的回复卡片,最终都进入同一个由宿主拥有的确切邮件审阅界面,由 Shell 绘制在卡片内(`crates/shell/src/mail_review.rs`);邮件的 Agent 只能提议发送(`mail.propose_send`)。只有对审阅界面上 Approve & Send 控件的一次可信按下和松开才会发送。目前这意味着 Android 上的物理触摸,由一个经审查的 Makepad 补丁提供(`tools/runtime-patches/makepad-trusted-user-input.patch`);桌面端和无障碍方式的批准推迟实现。开发者模式和常设规则都不能授权发送,`mail.send` 只会回答 `approval_required`。流程见[组合式邮件卡片](mail-composable-cards.zh-CN.md)。 -**首次使用同意**(`consent.rs`):应用第一次请求它的 Agent 时,Shell 显示该 Agent 可以读取和使用什么,以及模型在哪里运行;Settings 列出每个应用的 Agent 并提供关闭开关(ADR 0004 §4)。原生模块(`module_host.rs` 中在提供服务之前调用 `consent_for_module`)和脚本应用(`consent_for_contained`)**已在 main**。 +**常设规则**(`approvals/rules.rs`)可以要求收件人在联系人中或在本线程中、没有附件、由用户触发,或限定次数和金额;读不到所需事实的条件视为不满足。从面板创建的规则默认每天最多使用 20 次,“该应用的一切请求”这条最宽的规则最多持续 60 分钟,一次点按即可关闭所有规则。只有用户能创建规则。“收件人在联系人中”只有在用户允许后才使用邮件的数据(`approvals/contacts.rs`)。 -**谁在向路由提交请求。** AI 服务总线:面板对 `native-apps.json` 中列出的 `confirm: host` 工具(目前是 Terminal 的 `run`)的调用会被挂起(`bus::`),直到路由作答。以及 octos,经宿主工具中继(`host_tools::init` 在启动时用 `set_relay` 安装它):每个 `host_tool` 审批(有门控的 `confirm: host` 应用工具,只发给宿主连接),附所属应用、确切参数和调用方;以及每个 `confirm: app` 调用,先确认收到,再交给所属应用的面板;系统对话也以同样方式把其会话的审批交给路由。octos 在应用的 peer 会话或其某个上下文上发起的其他每个审批(octos 自己的工具,包括 `peer/input` 回合的),也作为该应用 Agent 对自己应用的调用交给同一个路由([#155](https://github.com/OctoSense-org/OctoSense/pull/155));应用的上下文只收到 `approval/handled_by_host`。只有没有路由的宿主才把它们留给应用:那时开发者模式为其覆盖的应用作答(`crates/app-peers/src/host_approvals.rs`,有审计),脚本应用的上下文则拒绝它们(`contained.rs`)。 +**开发者模式**(`crates/shell/src/dev_mode.rs`)只能由用户打开:在设置中输入确认短语,或在手机上使用开发者选项手势;也可以在启动时使用 `OCTOSENSE_DEV_MODE` 或 `--dev-grant-all`。发布构建只接受该启动参数,商店构建永远不能打开。 + +它覆盖全部应用或用户选定的应用。不在开发者专用的主目录中时,它在 8 小时后或重启时结束。开启期间,Shell 显示横幅,把每次调用审计到 `logs/dev-audit.jsonl`,并给所覆盖应用的 peer 添加 `dev.run`。它永远不作用于外部客户端,也不能发送邮件。 + +**输入。**审批路由接收经中转送来的内核 `host_tool` 审批;应用 peer 或其上下文上的其他所有审批,作为该应用 Agent 的调用(应用只会收到 `approval/handled_by_host`);系统对话的审批;以及 AI 面板对 `confirm: host` 工具的调用。 + +**时限。**Shell 持有的请求 10 分钟后过期:它被拒绝,绝不会被批准,并一直显示到用户关掉为止。如果 30 秒后这一轮仍在运行,代理会中断它。外部客户端的请求在 Shell 中永不过期。 + +**审计。**每个决定都是 `logs/approvals-audit.jsonl` 中的一行,记录的是参数摘要,而不是参数本身。 + +**首次使用同意**(`approvals/consent.rs`)展示 Agent 能读取和使用什么、模型在哪里运行。原生模块(`consent_for_module`)或脚本应用(`consent_for_contained`)要等用户允许后才能得到它的 Agent。 ## 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`,与助手服务的提供方式相同): +每个应用都有一套由宿主拥有的目录布局,在其 manifest 的 `storage` 块中声明,只由 Shell 计算(`crates/shell/src/app_storage/`,ADR 0004 §11): ``` /apps// 应用的 jail(App Hub 的 jail 根目录;原生应用的沙箱根目录) - accounts// 每个账号一个(应用没有账号时为 "device"): + accounts// 每个账号一个(应用不区分账号时为 "device"): 该账号的数据 = 该账号 Agent 的工作区 - common/ 与账号无关的应用数据 + 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 的工作区就是账号目录。** 自锁定 octos 665209e5 起新建的 peer 以它作为 `peer/prepare` 的 `cwd`(`host_tools::agent_workspace`,记录在其宿主 token 旁边);之前创建的 peer 保留内核分配的工作区,因为 octos 只在 peer 创建时的工作区下恢复它。目录名和记忆命名空间标签仍是同一账号的两种不同 hash([#139](https://github.com/OctoSense-org/OctoSense/issues/139))。 -- **机密**(`secrets.rs`,`AppStorage::secrets`):macOS 和 iOS 上用钥匙串(每个 profile、应用和键一项),其他平台是 `secrets//` 中每个键一个仅所有者可读(0600)的明文文件;测试、无界面运行和 `OCTOSENSE_SECRETS=file` 也使用文件存储。Windows、Linux 和 Android 的系统密钥库是待办项。机密从不放在 `apps/` 下。 -- **启动检查**(`check.rs`):任何 Agent 工作区都不能包含或通向 `secrets/`(工作区或 jail 本身是符号链接、指向机密的符号链接或硬链接、机密根目录位于其中)。被标记的工作区会被拒绝(`agent_workspace` 对该账号或所有账号返回 `Refused`),直到之后某次启动时检查通过;启动照常进行,不删除任何内容。 -- **清单到达宿主**(`app_storage/lifecycle.rs`):启动时读取 `native-apps.json` 的每个 `storage` 块(生成的 `NativeApp::storage`),脚本应用在安装和每次启动时读取其 `manifest.json` 的块(已安装应用的 `//bundle/`,系统应用最新的 `.system///`),用 `StorageSpec` 解析并以 `Storage::set_spec` 记录;`Storage::open` 据此为模块交接(`module_host.rs`)、进程应用的沙箱(`clients::sandbox_policy`)和 Card 启动布置 jail、账号文件夹、`common/`、`cache/` 和 `secrets//`。App Hub 所锁定版本的清单 schema 接受 `storage.max_bytes`、`accounts`、`agent_workspace` 和 `cache_max_bytes`(不接受 `external`,它只给经过评审的原生应用)。Mail 声明了 `accounts: true`,因此它的 Agent 代表其当前账号运行(用户最近一次登录的账号,来自 Mail 的宿主服务;`app_storage::lifecycle::contained_account_in`);其他脚本应用都按设备运行。 -- **配额**:原生应用打开时,后台线程测量其 jail 和 `cache/`(不跟随符号链接),超过 `storage.max_bytes` / `cache_max_bytes` 时记录警告;脚本应用的隔离环境自行执行配额。 -- **账号**来自真正拥有账号的地方:应用的助手服务(`OctosAppService::set_account`:Rinx 的 Matrix 登录和登出)通过 `app_peers::storage::account_changed` 报告每次变化,Mail 的宿主服务报告 `mail.add_account` / `mail.remove_account`(`octosense_mail_service::AccountEvent`)。登录会打开账号文件夹并解除暂停,然后 broker 才恢复 peer。**登出**(`Some(a)` → `None`)会暂停 Agent 而不是关闭它:broker 关闭该账号的请求上下文,`Storage::sign_out` 让工作区返回 `SignedOut`,工具调用返回 `signed_out`,不启动 `peer/input` 回合,已暂停账号的 peer 不会被准备。切换账号(`Some(a)` → `Some(b)`)会登录 `b`,不改变 `a`。没有账号的应用按设备运行,设备不会登出。 -- **移除账号**(Mail)会删除其文件夹;**卸载**(App Hub 删除 jail;Shell 在 `installed_app_changed` 中发现 jail 已不存在)会删除 `secrets//` 及其密钥库条目。随后两者都会抹除该 Agent:Shell 请 octos 对它记录的每个(应用,账号)peer 执行 `peer/purge`(octos#2649;在后台进行,`peer_purge_busy` 会重试),抹除其对话记录、记忆和黑板并释放绑定,并删除记录(`.peer`)。该账号保持暂停,并记录在宿主的 `secrets/.host/suspended.json`(0600,账号 id 已哈希)中,重启后依然有效,直到再次添加;那时它会得到一个新的 Agent(记录已删除)。清除成功后,设置 → 审批 → 应用 Agent 不再说明其记忆仍在;清除失败时保留记录以便之后再清除,设置会继续说明。对于记录尚未保存 peer 名称的旧记录,按 Shell 能推导出的名称(应用的标签和 id)清除;此时的 `peer_not_found` 视为失败,绝不视为成功。**退出登录**只会暂停:重新登录会恢复同一个 Agent。App Hub 当前固定的版本没有卸载按钮;加入后,它必须像 `take_completed_installs` 报告安装那样向 Shell 报告移除。 -- **Rinx** 声明 `accounts: true`,Shell 也向其模块提供该存储(`storage` 能力),但 Rinx 尚未领取:其数据保持原位(见下)。Rinx 使用按账号文件夹需要做的事(迁移要经过 hagency-org/Rinx#37 的规则:OctoSense 只锁定 Rinx 的发布标签,目前是 `v1.1.0`):在 `RinxModule::create` 中调用 `octosense_app_peers::storage::claim("rinx", &handles.scope.to_string())` 并保留该句柄;登录后,把其 Agent 需要读取的内容(导出的会话、共享的附件)写入 `storage.account_folder(Some())`;Matrix 存储和令牌通过 `storage.secrets()` 或放在 `secrets/rinx/` 下,绝不放在账号文件夹中;在迁移之前,现有数据继续使用 `app_data_dir()`。Shell 的 API 无需改动。 -- **内核自己的数据**是分开的: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 仍使用自己的数据目录(ADR 0004 §11,迁移经过 [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` 优先)。 +- **OctoSense 主目录**在手机上是平台提供的应用数据目录,其他情况下是 `~/.octosense`(`crates/shell/src/octosense/paths.rs`)。路径中出现符号链接会被拒绝;在 Unix 上,这些目录的权限是 0700。 +- **账号哈希**是对规范化后的账号 id 做带域分隔的 SHA-256,取其 128 位(`account_hash`)。每个账号文件夹都以它命名,所以修改它需要迁移。 +- **`storage` 块**(`accounts`、`agent_workspace`、`max_bytes`、`cache_max_bytes`;原生应用另有 `external`)在启动时从 `native-apps.json` 读取,脚本应用则在安装和每次启动时从其 manifest 读取(`app_storage/lifecycle.rs`)。邮件声明了 `accounts: true`。 +- **机密**从不放在 `apps/` 下(`app_storage/secrets.rs`)。macOS 和 iOS 把它们存入钥匙串;其他平台每个密钥一个文件,放在 `secrets//` 中,在 Unix 上仅所有者可读写(0600)。脚本应用只能通过宿主服务和宿主面板接触自己的机密。 +- **启动检查**(`app_storage/check.rs`)拒绝本身是符号链接、或链接到机密、或包含机密的工作区,直到之后某次启动发现它已干净。不会删除任何东西。 + +Rinx(通过 `OctosAppService::set_account`)和邮件的宿主服务会报告账号变化。删除账号会删除它的文件夹;卸载会删除应用的 jail、机密和钥匙串条目。然后 Shell 请 octos 对每个记录在案的 peer 执行 `peer/purge`(`crates/app-peers/src/purge.rs`),清除它的对话记录、记忆和黑板。该账号保持挂起(`secrets/.host/suspended.json`),直到再次添加,届时会得到一个新的 Agent。 + +内核的 core 目录在桌面端是 `~/.octosense/octos-home/.octos`,在手机上是 `/octos-home/.octos`(`crates/kernel/src/dirs.rs`),存放内核的 profile、会话、黑板和记忆。提供方密钥归 `llm` 宿主服务管理(见 [ai-services.zh-CN.md](ai-services.zh-CN.md#ai-providers-与-llm-宿主服务))。**规划中:**Rinx 目前还不认领这套存储,之后会把数据移到 `apps/rinx/` 下(ADR 0004 §11)。 ## 7. 信任边界与隔离 ``` 用户 ── 宿主面板(密钥、PIN、审批)──┐ v - +------------------------ Shell 进程(可信)-----------------------------+ - | 持有:宿主 token、peer 宿主 token、提供方密钥(经 llm)、机密 | - | 每次调用都检查:授权、同意、审批、审计 | - | +------------------+ +------------------------------------------+ | - | | 原生模块 | | Card runner:脚本应用各在隔离环境中 | | - | | 经评审,共享 | | (jail、配额、按授权 host.request) | | - | | 内存:受信任 | +------------------------------------------+ | - | +------------------+ | - +-------|------------------------------------------|---------------------+ - | hub(回环) | OUP,宿主 token + +------------------------ Shell 进程(可信) ------------------------------+ + | 持有:宿主 token、peer 宿主 token、提供方密钥(经 llm)、机密 | + | 每次调用都检查:授权、同意、审批、预算、审计 | + | +------------------+ +------------------------------------------+ | + | | 原生模块 | | Card runner:脚本应用各在隔离环境中 | | + | | 经审查,与 Shell | | (jail、配额、按授权 host.request) | | + | | 共享内存:可信 | +------------------------------------------+ | + | +------------------+ | + +-------|------------------------------------------|-----------------------+ + | hub(回环),每次启动一个密钥 | OUP,宿主 token +-------v----------+ +--------v--------------------+ | 进程应用 | | octos 内核 | - | (操作系统沙箱: | | 每个 peer 的工作区围栏 | + | (系统沙箱: | | 每个 peer 的工作区围栏 | | macOS、Linux) | | 外部客户端:允许列表 | +------------------+ +-----------------------------+ ``` -| 边界 | 由什么保证 | 状态 | -| --- | --- | --- | -| 脚本应用 ↔ 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) | macOS 已在 main(`crates/shell/src/sandbox/macos.rs`,Seatbelt 配置,已构建并测试;[#130](https://github.com/OctoSense-org/OctoSense/pull/130));Linux 的 Landlock 和 seccomp 已在 main(`crates/shell/src/sandbox/linux.rs`),2026-09-30 已在真实内核上验证([#138](https://github.com/OctoSense-org/OctoSense/issues/138),修复见 [#199](https://github.com/OctoSense-org/OctoSense/pull/199)、[#200](https://github.com/OctoSense-org/OctoSense/pull/200)、[#202](https://github.com/OctoSense-org/OctoSense/pull/202));它们的测试在本地 CI 的 Linux 主机作业 `linux-host / sandbox` 中运行([local-ci.md](local-ci.md),英文),不在 GitHub CI 中;Windows 规划中([#137](https://github.com/OctoSense-org/OctoSense/issues/137)):那里的进程应用以用户权限运行 | -| 应用 ↔ 内核 | 应用从不使用内核协议,**永远看不到宿主 token**:模块使用 peer link 或受限的 `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 | Card runner 的隔离环境、jail 和配额;`host.request` 只能访问已授权的服务族 | +| 原生模块 ↔ Shell | 内存上没有隔离:靠对第一方代码的审查,以及模块边界的 panic 捕获 | +| 进程应用 ↔ Shell | 独立的地址空间和系统沙箱:macOS 上是 Seatbelt,Linux 上是 Landlock 和 seccomp。**尚未实现:**Windows。 | +| 应用 ↔ 内核 | 没有应用直接使用 OUP,也没有应用看得到宿主 token | +| peer ↔ peer | octos 为每个 peer 提供独立的工作区(拒绝重叠)、记忆命名空间和对话记录 | +| Agent ↔ 机密 | 机密在所有 jail 和工作区之外;启动检查 | +| Agent ↔ 邮件投递 | 没有任何 Agent 工具或生成的卡片能发送:只有宿主对确切邮件的审阅,经可信的物理触摸批准后才发送 | +| 外部客户端 ↔ 内核 | 外部 token、方法与工具允许列表、`Host` 和 `Origin` 检查 | -**Shell 在每次调用时检查什么**(ADR 0004 §3):授权和同意;预算、速率限制和后台策略;每次工具调用的名称和参数是否符合声明的 `tools.json`;每个结果是否符合其 schema 和大小;自己的审计;崩溃清理。目前 main 上已有:同意;按精确名称授予 `octos.*` 服务(声明 ∩ 支持 ∩ 策略);脚本应用的参数规则和大小上限(文字最多 32 KiB,回复最多 2 MiB);总线调用的审批路由和审计。对应用工具调用(`crates/shell/src/host_tools/`):按(所属应用,工具)和调用方的授权、同意、已登出的账号、每次调用的参数是否符合工具声明的 `input_schema`(以及 64 KiB 上限)、每个结果是否符合其 `output_schema` 和 256 KiB 上限、每个调用方 Agent 的预算(每回合和每天的调用次数,来自 `native-apps.json` 的 `agent.budget`,默认 32 和 1000)、确认、每次出现只执行一次、取消后不再执行。预算之外的速率限制和后台策略尚未实现。用户在应用卡片中的操作不经过这个中继:它们走应用自己的路径(ADR 0004 §4),因此这些检查从不覆盖它们。 +**进程沙箱**(`crates/shell/src/sandbox/`)根据条目的 `sandbox` 和 `storage` 块构建。它封闭用户的主目录和各个卷,只开放应用的 jail、它的机密文件夹和经审查的 `external` 授权(例如 Terminal 的 `home:rw`),并让下一次构建要读取或运行的一切保持只读。`network: none` 只留下 hub 的端口,`processes: false` 禁止 fork 和 exec;应用只继承 Shell 环境变量中允许列表内的那些,从不包括密钥或 token。 -**谁都无法检查的**:原生应用的代码在它自己的工具里做了什么,或它为什么发起一个回合。控制手段是评审,以及进程应用的沙箱。 +**Shell 检查什么:**每次工具调用的检查就是中转的那份清单(见[第 4 节](#中转));脚本应用自己的 `octos.*` 调用则限定为确切的服务名、32 KiB 的文字和 2 MiB 的回复。**谁都无法检查的**,是原生应用在自己的工具里做了什么,或者它为什么发起一轮;对此靠审查,对进程应用还有沙箱。 ## 8. 完整示例:用邮件发送会议邀请 -这是 **ADR 0004 的历史草图,不是已实现的发信流程**。Calendar 提供 `calendar.add_event`;Mail 现有账户绑定读取、草稿/提议操作和卡片发布,但没有声明可共享的 Agent 工具 `mail.send`。ADR0007 已取代草图中假设的常设规则发信方式:当前 Mail 必须由用户实体批准确切的宿主审核。下图只说明当时提出的跨应用中继形态,不代表当前 Mail 的审批契约。实际回复流程见[组合 Mail 卡片](mail-composable-cards.zh-CN.md)。 +这是 ADR 0004 对跨应用协作的最初设想:先调用日历已有的 `calendar.add_event`,再跨应用调用 `mail.send`。邮件这一半**尚未实现**,而且 ADR 0007 已经改变了它的批准方式:任何常设规则或开发者模式都不能批准发送邮件,每封邮件都需要宿主的审阅和一次物理触摸(见[第 5 节](#5-审批))。下图保留这个设想中的中转路径,并标出被 ADR 0007 取代的部分。邮件现在如何发送,见[组合式邮件卡片](mail-composable-cards.zh-CN.md)。 ```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(宿主服务) + participant C as 日历的 Agent(peer) + participant R as Shell:审批 + participant M as 邮件(宿主服务) P->>S: "邀请 Ana、Bo 和 Edward 周二下午 3 点开会" S->>C: peer_send_input(任务说明) - Note over S,SH: octos 把 peer/input 送到宿主 - SH->>C: turn/start(应用的工具、记忆、上下文) + Note over S,SH: octos 把 peer/input 送到 Shell + SH->>C: turn/start(日历的工具、记忆、账号) C->>SH: peer/tool/call calendar.add_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 + C->>SH: peer/tool/call mail.send x3(尚未实现,调用方:日历) + SH->>SH: 授权检查:日历是否获授权 mail.send? + SH->>R: 审批(邮件、mail.send、确切参数、调用方日历) + Note over R,P: ADR 0004 允许常设规则或面板批准这一步。
ADR 0007 取代了它:任何规则或开发者模式都不能批准发送邮件。 + R->>P: 宿主对每封确切邮件的审阅 + P-->>R: 逐封批准,用物理触摸 + SH->>M: 发送 x3(尚未实现) M-->>SH: 结果 SH-->>C: peer/tool/result C->>C: octos 写入 peers/(slug)/result.md @@ -444,57 +444,59 @@ sequenceDiagram S->>P: "已预订周二下午 3 点;已向 3 人发送邀请" ``` -| 步骤 | 发生什么 | 状态 | -| --- | --- | --- | -| 1 | 用户在系统对话 `_main:api:octosense#system` 中向系统 Agent 提出请求。 | 已在 main:Shell 的系统对话([#132](https://github.com/OctoSense-org/OctoSense/pull/132)),或已配对的 Talk to Octos 客户端(系统助手面板与应用的 Makepad `aichat` 总线接口分别实现) | -| 2 | 系统 Agent 做计划。有歧义就提问,不去猜(“两个 Edward?”);有限的读取可以直接调用获授权的工具。需要 Calendar 自己判断的工作用 `peer_send_input` 交给 Calendar 的 Agent;如果用户还没有允许 Calendar 的 Agent,`agents.ask` 会向用户显示首次使用面板并等待回答。 | `peer_send_input`、`agents.list` 和 `agents.ask` 已在 main,relay 授权也已实现;系统 Agent 的宿主工具见第 4 节,此示例的 Mail 工具和授权尚未实现 | -| 3 | octos 把输入以 `peer/input` 送到 Shell;Shell 在 Calendar 的 peer 上用 `turn/start` 启动回合,使其带有 Calendar 的工具、记忆和账号上下文。如果 Calendar 的 peer 没有宿主连接,系统 Agent 会被告知该应用未连接。 | 已在 main(锁定版本含 [octos#2567](https://github.com/octos-org/octos/pull/2567);由 broker 启动回合) | -| 4 | Calendar 的 Agent 调用 `calendar.add_event`(自己的工具,`act`:运行时不弹面板),并为每位受邀者调用一次 Mail 可共享的 `mail.send`。每次调用都以 `peer/tool/call` 到达 Shell;Shell 盖上调用方(Calendar 的 Agent)、账号和上下文。 | Calendar 操作(`calendar` 宿主服务把日程保存在 `/calendar/events.json`)与中继已实现;`mail.send` 规划中 | -| 5 | Shell 检查 Calendar 的清单是否获授权 `mail.send`(脚本应用在安装时授权);不需要第二道 Agent 级别的同意。 | Relay 与 App Hub 准入检查已实现;此特定授权/工具尚不存在 | -| 6 | `mail.send` 是对外的 `confirm: host` 工具,由审批路由处理:开发者模式未开启;不是 `confirm: app`;不是 `auto_approvable: false`;如果有针对(Mail,`mail.send`)的常设规则(例如“发给我的联系人”),就由规则批准(通知并审计;只有用户打开了“在审批规则中使用我的联系人”,联系人条件才会成立),否则一个合并的 Shell 面板列出每封邀请:所属应用 Mail、工具 `mail.send`、调用方应用 Calendar 以及确切参数。 | 路由、规则、面板和审计已在 main([#120](https://github.com/OctoSense-org/OctoSense/pull/120));经中继由内核的 `host_tool` 审批提交请求,Calendar 的破坏性工具 `calendar.remove_event` 如今就走这条路径;`mail.send` 还在规划中 | -| 7 | 批准后,Shell 把每次调用交给 Mail 的宿主服务,它用用户在 Mail 宿主面板上登录的账号发送(密码永远不会到达 Agent),并把结果以 `peer/tool/result` 返回内核。 | Mail 宿主服务已实现(旧版 `mail.send` 入口也要求宿主审核);Agent 已有 `mail.notify`,`mail.send` 作为 Agent 工具仍在规划中 | -| 8 | Calendar 的回合结束,octos 写入 `peers//result.md` 和 `turns.txt`;系统 Agent 用 `peer_gather` 读取。失败的邀请会被点名,结果未知的邀请未经用户同意绝不重试。 | 黑板已在 main(octos) | -| 9 | 系统 Agent 宣布“已预订周二下午 3 点;已向 3 人发送邀请”。Calendar 和 Mail 自己的界面会显示变化,因为它们的数据变了。 | 规划中:Calendar 自己的窗口还不能列出日程(它需要 App Hub 的 `calendar` 能力);如今 `calendar.notify` 可以把日程放到 glance 屏幕上 | +- 系统 Agent 遇到歧义会先问(“有两个 Edward?”),而不是去猜。如果日历的 Agent 还没获准,`agents.ask` 会先弹出首次使用面板。 +- `calendar.add_event` 是 `act`,所以无需面板即可运行。桌面端和手机都附带日历。 +- 授权会沿用现有机制:`mail.send` 作为日历 `agent.tools` 中一个带点的名称,在安装时授予。邮件并没有声明这个工具。 +- ADR 0004 让审批路由决定每次发送,所以像“收件人都在我的联系人中”这样的常设规则可以批准它,否则每封邀请各有一个面板。ADR 0007 为邮件取代了这一做法:不论谁提议发送,最终都进入宿主对确切邮件的审阅(见[第 5 节](#5-审批))。邮件的 Agent 只能提议发送(`mail.propose_send`)。 +- 邮件的宿主服务用用户登录的账号发送;密码永远不会到达 Agent。它的 `mail.send` 方法现在只会回答 `approval_required`。 +- 结果未知的发送绝不会自动重发;重试需要重新审阅并批准。 +- **尚未实现:**日历的窗口还不能列出日程;`calendar.notify` 可以把日程放到 glance 屏幕上。 ## 代码与 ADR 不一致之处 -撰写本文时(2026-09-28)发现,2026-09-29 对照 `main` 复查(ADR 0004 差距审查),2026-10-02 再次复查;标为已修复的条目此后已在代码或 ADR 文本中修复。 - -1. **Terminal 之外的进程应用。** 已修复:可选的 Sheets 和 Reference 在所有目标上都是 `module`,生成器拒绝 Linux 上的普通 `process`(ADR 0004 §2)。 -2. **崩溃进程应用的重启。** 已修复([#130](https://github.com/OctoSense-org/OctoSense/pull/130)):意外退出时保留磁贴,显示为已关闭并提供 Restart(`ClientSlot::stops_in_place`;测试 `an_unexpected_death_keeps_the_tile_closed_with_restart`)。 -3. **Agent 工作区 = 账号目录。** 部分修复:自锁定 octos 665209e5 起新建的 peer 以账号目录作为 `peer/prepare` 的 `cwd`;之前创建的 peer 保留内核分配的工作区,因为 octos 只允许在创建时的工作区下恢复 peer(迁移需要 octos 尚未提供的支持)。账号目录名(`account_hash`,SHA-256)和记忆命名空间标签(`broker.rs` 中的 FNV-1a)仍是对账号的两种不同 hash;改动标签会让每个应用的记忆重新计算键([#139](https://github.com/OctoSense-org/OctoSense/issues/139))。 -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。文本已修复:ADR 0003 带有一条注明日期的修订(2026-09-29),ADR 0002 §12 及其修订也是。(已修复,G13:当 `enforce` 拒绝外来的策略或用户自己的 octos 主目录,或者策略读回不一致时,内核不会启动,并告诉每个使用方原因;每个应用 peer 恰好保留其被授予的 `generic_tools`,从不包括 shell。) -5. **过时的背景描述。** 已修复:ADR 0004 的背景表格和各 README 的目录结构表不再列出 ADR 0004 步骤 1 删除的原生 News、Maps 和 Photos 模块(#113)。[ai-services.zh-CN.md](ai-services.zh-CN.md) 现已描述 2026-09-28 的 `main`(#106 的 Card runner `octos` 服务、#120 的同意机制;当前 `Policy::contained_gate` 默认是 `ContainedGate::Consent`,不是全局关闭)。 -6. **“在 Settings 中开启”命令执行。** 已修复:Setup › Assistant › Command execution 设置它(#132),开启期间系统对话把 `terminal.run` 注册到系统会话。 -7. **`host::processes_available()` 的测试。** 已修复:`the_desk_knows_where_it_runs` 除了 `wasm32` 也检查手机目标平台。 -8. **审批,ADR 0004 §8。** 每次应用工具调用都通过 `peer/tool/call` 到达 Shell,每个有门控的调用都到达路由(见上文)。还没有应用注册自己的 `confirm: app` 面板(Rinx 需要通过 `OctosAppService::set_confirm_sheet` 交出它的发送面板;进程应用的面板在其 peer link 打开时注册,但还没有进程应用使用 peer link),因此这类调用会等待后被拒绝。main 上唯一的 `confirm: app` 工具是 Shell 自己的 `agents.ask`,由系统对话自己持有。审计记录的是参数摘要而不是参数。发送队列和撤销窗口尚未实现。 -9. **存储,ADR 0004 §11。** 机密只在 macOS 和 iOS 上使用系统钥匙串(其他平台为 0600 明文文件)。启动检查拒绝通过链接或包含关系通向机密的工作区,而不是查找 `secrets/` 路径,并且不会中止启动。(已修复:app storage 和同意面板中 `storage.accounts` 都默认为 `false`,同意面板现在读取 `StorageSpec`。) -10. **开发者模式,ADR 0004 §13。** 已修复:`dev.run` 已注册在被覆盖应用的 peer 上,由 shell 执行。已修复:Settings 可以选择覆盖哪些应用(桌面端在 Setup › Developer options,手机上在 About phone › Developer options),手机上连续点按 Build number 七次显示 Developer options,再在那里确认开启。未完成:进程内模块仍会显示自己的确认面板。 -11. **提问,ADR 0004 §6(2026-09-29 决定)。** Agent 用 octos 的 `ask_user_question` 提问,Shell 把每个问题转到应用的对话或系统对话;`host.ask` 已放弃。已修复:broker 把应用 peer 的 `user_question/requested` 转给 Shell 的请求模型(`crates/shell/src/questions/`),请求模型按回合的发起者把它转到应用的对话或系统对话;只有用户能在 Shell 界面上回答(卡片从不回答)。只有当 Agent 的 `generic_tools` 列出 `ask_user_question` 时它才会提问。 -12. **进程沙箱,ADR 0004 §3。** Linux 已修复:Landlock 和 seccomp 已于 2026-09-30 在真实内核上验证(#138,修复见 #199、#200 和 #202),它们的测试在本地 CI 的 `linux-host / sandbox` 作业中运行,不在 GitHub CI 中。未完成:Windows 尚未实现(#137)。 -13. **2026-09-29 应用 Agent 审查的后续,2026-10-01 复核。** “所有审批被丢弃”“全部固定为 `device`”“输入启动失败只记日志”等旧描述已不符合当前实现。`broker.rs::on_user_question` 和审批路由把问题与审批交给宿主;`contained.rs::account_of` 获取宿主账号;失败的系统输入使用 `peer/input/reject`;`driver_of`/`take_over` 协调同一 peer 的多个 broker,`broker` 测试覆盖了这些路径。受限 `host.request` 适配器仍返回汇总回合回复,没有脚本可订阅的流式接口;原生 context 和 Shell 聊天则有事件订阅。`implemented_by: "app"` 的脚本工具执行、`AGENT.md` 提示词加载和自动技能/触发器仍未实现。确切边界见[代码导读](architecture-walkthrough.zh-CN.md)。 +下面每一项都是一个 ADR 决定,而 HEAD 上的代码没有照做。 + +1. **声明了 `agent_workspace: "none"` 的原生 Agent 仍有工作区。**ADR 0004 §11 说这样的 Agent 不读取任何文件。但 `host_tools::agent_workspace_in` 会给每个拥有 `octos.*` 服务的原生应用分配账号文件夹作为 peer 的工作区,不管 `agent_workspace` 声明了什么;而 Shell 会在每个有工作区的 peer 上注册 `files.*`(Unix)。所以 Calculator、Clock、Notes、Reminders、Weather 和 Terminal 都有一个文件夹和 `files.*`。这项声明只在两处起作用:脚本应用,以及用户的通道能否只读地查看该文件夹(`context_reads_account`)。 +2. **旧 peer 与两种哈希。**ADR 0004 §11 让账号文件夹成为每个 peer 的工作区。记录中没有工作区的 peer,若 octos 接受,就以账号文件夹恢复,否则以内核当初分配的工作区恢复。文件夹名(SHA-256)和记忆标签(FNV-1a,`account_tag`)仍是对同一个规范化账号的两种哈希。 +3. **平台密钥库中的机密。**ADR 0004 §11 要求在有系统钥匙串的平台上使用它。只有 macOS 和 iOS 用了钥匙串。Windows、Linux、Android 和 OpenHarmony 把每个密钥存成普通文件,除 Windows 外都仅所有者可读写(0600)。 +4. **频率限制与后台策略。**ADR 0004 §3 把两者列为中转的检查项;中转只执行预算。 +5. **事后控制。**ADR 0004 §8 承诺提供带撤销窗口的发送队列,并在审计中记录自动批准的参数。目前既没有队列也没有撤销,审计只保存参数摘要。 +6. **按客户端的文件授权。**ADR 0004 §11 按应用的按客户端授权来收窄宿主读取工具;目前还没有 manifest 字段能声明这类授权。 +7. **Rinx。**ADR 0004 §9 让 Rinx 的发送工具在它自己的 `confirm: app` 面板上确认,§11 把它的数据移到 `apps/rinx/` 下,§13 让开发者模式覆盖每个应用的面板。在其锁定的版本中,Rinx 不声明任何 Agent 工具,不向审批路由注册面板,仍使用自己的数据文件夹,而且在开发者模式下它的发送面板仍会询问。 +8. **Windows 沙箱。**ADR 0004 §3 为 Windows 上的进程应用提供 AppContainer。它还没有实现,所以这些应用以用户的权限运行,Windows 上也从不提供 `terminal.run`。 +9. **发布包中的进程应用。**ADR 0004 §2 要求附带它们的二进制;发布包只附带 `octosense` 和内核(`desktop/packaging/release.json`)。 +10. **发送邮件。**ADR 0004 §8 允许常设规则回答 `mail.send` 这类对外调用,§13 允许开发者模式覆盖所有审批。代码遵循的是 ADR 0007:不论常设规则或开发者模式怎么说,发送邮件都需要宿主的审阅和一次物理触摸;ADR 0004 没有提到这个例外。 +11. **进程内的 Terminal 仍提供 `run`。**ADR 0004 §10 规定进程内的 Terminal 只读。但 Shell 用另一个执行器替换了它模块自带的只读执行器,新执行器也提供 `run`(`crates/shell/src/module_host.rs` 中的 `host_executor`,有测试固定这一行为)。所以只要桌面端在进程内运行 Terminal(没有 Vulkan 和 Wayland 的 Linux,或被切换为模块时),桌面端的 AI 面板仍能经审批路由输入命令。系统 Agent 的 `terminal.run` 则符合 ADR:它要求 Terminal 作为沙箱进程运行。 ## 源码位置 | 内容 | 位置 | | --- | --- | | 原生应用清单、生成器、生成的表 | [`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) | -| 哪些应用有 Agent;系统 Agent 的 `agents.list` 和 `agents.ask` | [`crates/shell/src/apps.rs`](../crates/shell/src/apps.rs)(`agent_apps`)、[`crates/shell/src/agents.rs`](../crates/shell/src/agents.rs) | -| 系统对话和 “Ask ” 面板 | [`crates/shell/src/system_chat/`](../crates/shell/src/system_chat/mod.rs)、[`crates/shell/src/app_chat/`](../crates/shell/src/app_chat/mod.rs) | -| 宿主工具中继:应用工具、脚本应用的执行器、宿主读取工具、工具箱 | [`crates/shell/src/host_tools/`](../crates/shell/src/host_tools/mod.rs)(`relay.rs`、`script_apps.rs`、`files.rs`、`toolbox.rs`)、[`crates/ai-host/src/toolbox_peers.rs`](../crates/ai-host/src/toolbox_peers.rs)、[`crates/toolbox`](../crates/toolbox/README.md) | +| 托管方式、宿主服务、哪些应用有 Agent | [`crates/shell/src/apps.rs`](../crates/shell/src/apps.rs) | +| 进程应用、hub、沙箱 | [`crates/shell/src/clients.rs`](../crates/shell/src/clients.rs)、[`hub.rs`](../crates/shell/src/hub.rs)、[`sandbox/`](../crates/shell/src/sandbox/mod.rs) | +| 进程内模块、panic 捕获 | [`crates/shell/src/module_host.rs`](../crates/shell/src/module_host.rs) | +| 内核服务、Talk to Octos、帧路由、系统 Agent 的工具 | [`crates/kernel/src/`](../crates/kernel/README.zh-CN.md)(`launch.rs`、`lib.rs`、`kernel.rs`、`dirs.rs`、`network.rs`、`router.rs`、`system_tools.rs`) | +| Shell 的 AI 入口;脚本应用的 `octos` 服务;模块的 peer link | [`crates/ai-host/src/`](../crates/ai-host/README.md)(`lib.rs`、`contained.rs`、`module_peer.rs`) | +| 应用 peer:契约、代理、启动、注入、指导、清除 | [`crates/app-peers/src/`](../crates/app-peers/README.md) | +| 系统 Agent 的宿主工具;邮件的事件 | [`crates/shell/src/agents.rs`](../crates/shell/src/agents.rs)、[`agent_events.rs`](../crates/shell/src/agent_events.rs)、[`system_chat/grants.rs`](../crates/shell/src/system_chat/grants.rs) | +| 系统对话和 “Ask <app>” 面板 | [`crates/shell/src/system_chat/`](../crates/shell/src/system_chat/mod.rs)、[`app_chat/`](../crates/shell/src/app_chat/mod.rs) | +| peer link | [`crates/shell/src/peer_link/`](../crates/shell/src/peer_link/mod.rs) | +| 宿主工具中转、执行器、宿主读取工具、`dev.run`、工具箱 | [`crates/shell/src/host_tools/`](../crates/shell/src/host_tools/mod.rs)、[`crates/toolbox`](../crates/toolbox/README.md) | +| AI 服务总线 | [`crates/shell/src/ai_bus.rs`](../crates/shell/src/ai_bus.rs) | | Agent 的提问 | [`crates/shell/src/questions/`](../crates/shell/src/questions/mod.rs) | -| Glance 卡片:服务、通知卡片、磁贴、面板、卡片窗口、卡内对话、toast | [`crates/shell/src/glance.rs`](../crates/shell/src/glance.rs)、[`glance_notice.rs`](../crates/shell/src/glance_notice.rs)、[`glance_card.rs`](../crates/shell/src/glance_card.rs)、[`glance_panel.rs`](../crates/shell/src/glance_panel.rs)、[`glance_sheet.rs`](../crates/shell/src/glance_sheet.rs)、[`glance_chat.rs`](../crates/shell/src/glance_chat.rs)、[`shell/notifications.rs`](../crates/shell/src/shell/notifications.rs);[`crates/l0-chat`](../crates/l0-chat/README.md) | -| 系统应用的 Agent 工具和宿主服务 | `apps//bundle/tools.json`;[`apps/mail/host-service`](../apps/mail/host-service/src/lib.rs)、[`apps/calendar/host-service`](../apps/calendar/host-service/src/lib.rs)、[`apps/news/host-service`](../apps/news/host-service/README.md) | -| 进程沙箱 | [`crates/shell/src/sandbox/`](../crates/shell/src/sandbox/mod.rs)(`macos.rs`、`linux.rs`) | -| 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](https://github.com/octos-org/octos/blob/main/docs/OCTOS_UI_PROTOCOL_CHANGE_REQUEST_UPCR_2026_035_PEER_HOST_TOOLS.md)([octos#2567](https://github.com/octos-org/octos/pull/2567))、[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) | +| 审批、同意、联系人、审计 | [`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) | +| glance 卡片和卡内对话 | [`crates/shell/src/glance.rs`](../crates/shell/src/glance.rs)、[`glance_notice.rs`](../crates/shell/src/glance_notice.rs)、[`glance_card.rs`](../crates/shell/src/glance_card.rs)、[`crates/l0-chat`](../crates/l0-chat/README.md) | +| 系统应用及其工具和宿主服务 | [`desktop/system-apps.json`](../desktop/system-apps.json)、[`phone/system-apps.json`](../phone/system-apps.json)、`apps//bundle/tools.json`、`apps//host-service/` | + +本仓库之外的代码,按 `Cargo.toml` 锁定的版本: + +| 内容 | 位置 | +| --- | --- | +| Makepad:`OctosPeer` 客户端;各原生应用的 Agent 代码 | [`libs/ai/services/src/peer.rs`](https://github.com/OctoSense-org/makepad/blob/c155f61d0e1600d2ec474209374444a38a09a470/libs/ai/services/src/peer.rs)、`apps//src/view.rs` | +| Terminal 自己的 Agent 链接,本仓库应用到 Makepad 上的一个经审查补丁 | [`tools/runtime-patches/makepad-terminal-own-agent.patch`](../tools/runtime-patches/makepad-terminal-own-agent.patch) | +| octos:两条通道的共享历史(20 条,16 KiB);`peer_send_input`(64 KiB) | [`crates/octos-cli/src/peers/shared_history.rs`](https://github.com/octos-org/octos/blob/056173e85b150e387805fc307fe231064ac1ed35/crates/octos-cli/src/peers/shared_history.rs)、[`crates/octos-agent/src/tools/peer_send_input.rs`](https://github.com/octos-org/octos/blob/056173e85b150e387805fc307fe231064ac1ed35/crates/octos-agent/src/tools/peer_send_input.rs) | +| octos:宿主管理的 serve、应用 peer、peer 宿主工具 | [`docs/HOST_MANAGED_SERVE.md`](https://github.com/octos-org/octos/blob/056173e85b150e387805fc307fe231064ac1ed35/docs/HOST_MANAGED_SERVE.md)、[UPCR-2026-034](https://github.com/octos-org/octos/blob/056173e85b150e387805fc307fe231064ac1ed35/docs/OCTOS_UI_PROTOCOL_CHANGE_REQUEST_UPCR_2026_034_HOST_APP_PEERS.md)、[UPCR-2026-035](https://github.com/octos-org/octos/blob/056173e85b150e387805fc307fe231064ac1ed35/docs/OCTOS_UI_PROTOCOL_CHANGE_REQUEST_UPCR_2026_035_PEER_HOST_TOOLS.md)、[UPCR-2026-036](https://github.com/octos-org/octos/blob/056173e85b150e387805fc307fe231064ac1ed35/docs/OCTOS_UI_PROTOCOL_CHANGE_REQUEST_UPCR_2026_036_HOST_MANAGED_SERVE.md) | +| App Hub:脚本应用可以保留的内核工具(`KERNEL_TOOLS`) | [`crates/app-policy/src/policy.rs`](https://github.com/OctoSense-org/OctoSense-App-Hub/blob/0d5b47a2ae9eb98020feca26b7c895a3cf797dc1/crates/app-policy/src/policy.rs) | diff --git a/docs/images/ai-services-cross-app-call.png b/docs/images/ai-services-cross-app-call.png index 52aa4e67..9a8ecfb9 100644 Binary files a/docs/images/ai-services-cross-app-call.png and b/docs/images/ai-services-cross-app-call.png differ diff --git a/docs/images/ai-services-processes.png b/docs/images/ai-services-processes.png index d5e67755..981ca391 100644 Binary files a/docs/images/ai-services-processes.png and b/docs/images/ai-services-processes.png differ diff --git a/docs/local-ci.md b/docs/local-ci.md index 1a588d94..2fd1b6b9 100644 --- a/docs/local-ci.md +++ b/docs/local-ci.md @@ -1,6 +1,6 @@ # Local CI -`tools/ci-local.sh` runs the checks of `.github/workflows/` (desktop, phone, apps, rom) on your machine, so a pull request can be merged on a local pass while the GitHub macOS runner queue is saturated. GitHub CI stays on: it runs on every push to `main`, including these merges, and a red `main` is fixed before anything else is merged. +`tools/ci-local.sh` runs the checks of the desktop, phone, apps and rom workflows locally. When the GitHub macOS runner queue is saturated, a pull request may be [merged on a local pass](#merge-on-a-local-pass). GitHub CI stays on and runs on these merges too; a red `main` is fixed before anything else is merged. ## Run it @@ -10,30 +10,53 @@ tools/ci-local.sh --only all # or desktop | phone | apps | rom, co tools/ci-local.sh --only phone --keep-going --jobs 8 tools/ci-local.sh --list # the plan; runs nothing tools/ci-local.sh --check-drift # the local mapping still fits the workflows -tools/ci-local.sh --only apps --linux-host # the ubuntu jobs on the Linux build host (below) ``` -- **Same commands as GitHub.** The runner reads the workflow files and runs each job's `run:` steps verbatim: same working directory, same `env:`, same `bash --noprofile --norc -eo pipefail`. Nothing is copied, so nothing drifts. The few things it maps (the actions: checkout, setup-python, rust-toolchain, rust-cache, the kernel cache, setup-node; expressions like `${{ runner.temp }}`) are checked by `--check-drift`. Every run checks them too, and so does `tools/test_ci_local.py` (part of the desktop job). A workflow that starts using a new action, job-level `if:`, a matrix or an unknown expression fails that check until `tools/ci_local.py` is taught about it. The one workflow left out on purpose is `release-desktop.yml` (`NOT_LOCAL` in `tools/ci_local.py`, with its reason): a release workflow with a matrix, environments, secrets and artifacts. ci-local never runs it and `--check-drift` skips it; its packaging scripts' tests (`tools/test_release_scan.py`, `desktop/scripts/test_package.py`) run in the desktop job. -- **Output.** Each step is printed as PASS, FAIL, SKIPPED or NOT RUN (after a failure, without `--keep-going`), followed by a summary table with times. The full output goes to `target/ci-local/.log`. `target/ci-local/last.json` records the commit, whether the tree was dirty, the workflows run, and every step's result. The exit status is non-zero on any FAIL. -- **Skips are never passes.** A step that cannot run here is SKIPPED and the summary says why. Today that is the rom product tests, unless a JDK works (`javac -version`, from `PATH` or `JAVA_HOME`; macOS's `/usr/bin/javac` is only a stub), the rom `Check generated Agent Binder client` step, unless the Android SDK has `build-tools;35.0.0` and `platforms;android-35` (found through `ANDROID_HOME`), and the web installer job, unless `node`/`npm`/`npx` are on `PATH`. Without `--linux-host`, jobs GitHub runs on `ubuntu-latest` (apps `services` and `kernel-security`, rom) run on the Mac. Their `#[cfg(target_os = "linux")]` code is not exercised, and the summary notes this. -- **The octos kernel.** The real-kernel steps (phone's app-peers relay and two-lane scenario, apps' `kernel-security`) need `octos` at the revision `Cargo.lock` pins. The workflow's own `Build octos (unless cached)` step builds it once (`tools/kernel-artifact.py --host`). The binary is then kept in a per-user cache that every clone shares, keyed by OS, architecture and octos revision (`~/Library/Caches/octosense-ci-local/octos-kernel/`, `$XDG_CACHE_HOME` or `OCTOSENSE_CI_LOCAL_CACHE`). The copy is written atomically, under a per-revision lock, so two clones never build the same kernel at once. -- **Sharing the machine.** At most two runs execute at once (`--slots N` or `OCTOSENSE_CI_LOCAL_SLOTS`), coordinated by mkdir locks in `${TMPDIR}/octosense-ci-local/` (or `OCTOSENSE_CI_LOCAL_LOCKS`). A run waits for a free slot and says so. With `--no-wait` it exits with status 75 instead. A slot whose owner process has died is taken over. `--jobs N` sets `CARGO_BUILD_JOBS` (default: half the CPUs). -- **Tools.** `cargo` (with clippy and rustfmt) comes from `PATH`, or `~/.cargo/bin` if it is not on `PATH`. `python3` also comes from `PATH`: GitHub uses 3.12, and a different version is noted in the summary. `cmake` is not needed by any of these steps. If a build ever asks for it, put one on `PATH`, e.g. `python3 -m venv && /bin/pip install cmake`. +**The same commands as GitHub.** `tools/ci_local.py` reads `.github/workflows/` and runs each job's `run:` steps verbatim, with the step's working directory and `env:`, in GitHub's `bash --noprofile --norc -eo pipefail`. Nothing is copied, so nothing drifts. GitHub's actions map to local stand-ins: the working tree, this clone's `target/`, a shared kernel cache and the tools on `PATH`. `--check-drift`, every run and `tools/test_ci_local.py` check that mapping: a workflow that starts using something the runner does not model, such as a new action or a matrix, fails until `tools/ci_local.py` learns it. + +**One workflow is left out:** `release-desktop.yml`, a release workflow with a matrix, environments and secrets (`NOT_LOCAL` gives the reason). The desktop job runs its packaging scripts' tests. GitHub still runs its `plan` and `package` jobs on pull requests that change the packaging; the merge script ignores them. + +**Output.** Each step prints PASS, FAIL, SKIPPED or NOT RUN (after a failure, without `--keep-going`), then a summary table with times. The full log goes to `target/ci-local/.log`, and `target/ci-local/last.json` records the commit, whether the tree was dirty, the workflows run and each step's result. Any FAIL makes the exit status non-zero. + +**Skips are never passes.** A step that cannot run here is SKIPPED with the reason. A run with skips still exits 0, but in a workflow the pull request triggers, the merge refuses a skip like a failure. On a Mac that usually means rom: + +- its product tests need a JDK (`javac -version` must work, from `PATH` or `JAVA_HOME`; macOS's `/usr/bin/javac` is only a stub); +- `Check generated Agent Binder client` needs the Android SDK's `build-tools;35.0.0` and `platforms;android-35` (`ANDROID_HOME`); +- the web installer needs `node`, `npm` and `npx`. + +Without `--linux-host`, the ubuntu jobs (apps' `services` and `kernel-security`, both rom jobs) run on the Mac, and the summary notes that the apps jobs' `#[cfg(target_os = "linux")]` code went untested. + +**The octos kernel.** The real-kernel steps in phone and apps need `octos` at the revision `Cargo.lock` pins. The workflow's `Build octos (unless cached)` step builds it once (`tools/kernel-artifact.py --host`), and the runner keeps it in a per-user cache that every clone shares (`~/Library/Caches/octosense-ci-local/octos-kernel/` on macOS). A lock per revision stops two clones from building it at once. + +**Sharing the machine.** At most two runs execute at once (`--slots N`). Another run waits and says so, or with `--no-wait` exits with status 75. The slots are mkdir locks in `$TMPDIR/octosense-ci-local/`, taken over when their owner has died. A second run in the same clone exits with 75 at once. `--jobs N` sets `CARGO_BUILD_JOBS` (default: half the CPUs). ## Run the Linux jobs on the Linux build host -`--linux-host` runs every job whose `runs-on` is ubuntu on the Linux build host configured in `~/.config/octosense/build.env` (`OCTOSENSE_BUILD_HOST`, the ssh target, and `OCTOSENSE_BUILD_KEY`, its key; the environment overrides the file, and `--linux-host ` names another target). The macOS jobs still run on the Mac, at the same time. It also adds the Linux-only checks no workflow runs: today the process sandbox's Landlock and seccomp tests (`cargo test --locked -p octosense-shell --lib sandbox::`, the `#[cfg(target_os = "linux")]` tests in `crates/shell/src/sandbox/`), as the job `linux-host / sandbox`. They come with any run that includes a workflow a change to the shell triggers (desktop, phone, apps). On a kernel without Landlock the tests skip themselves; that step then fails instead of passing. +`--linux-host` sends every job whose `runs-on` is ubuntu to the Linux build host set in `~/.config/octosense/build.env`: `OCTOSENSE_BUILD_HOST` is the ssh target and `OCTOSENSE_BUILD_KEY` its key. The environment overrides the file, and `--linux-host ` names another target. The macOS jobs stay on the Mac, in parallel. + +It also adds a Linux-only check, which no workflow runs: the process sandbox's Landlock and seccomp tests (`cargo test --locked -p octosense-shell --lib sandbox::`), as the job `linux-host / sandbox`, in any run that includes desktop, phone or apps. On a kernel without Landlock the tests skip themselves, so the step fails instead of passing. ```sh tools/ci-local.sh --only apps --linux-host # services, kernel-security and the sandbox tests on Linux; apps on the Mac tools/ci-local.sh --only all --linux-host --list # the plan, with where each job runs ``` -- **What runs there.** The exact commit under test (`HEAD`; uncommitted changes stay on the Mac, and the run is marked dirty as usual) goes to the host as a git bundle, incrementally after the first run, into `~/octosense-ci/repo.git`. It is checked out as `~/octosense-ci/runs/-`, and the host runs `tools/setup.py` itself from the same lock files (its sources are worktrees of hub clones in `~/octosense-ci/cache/hub/`). The newest five run directories are kept. The host then runs this checkout's own `tools/ci_local.py`, so the same `run:` steps run there, extracted from the same workflow files. -- **Results.** Each remote step appears in the same PASS/FAIL/SKIPPED table, marked `(linux)`, and in `last.json` with `"host": "linux"` and `"sha"`, the commit the host checked out. The summary's `linux_host` records that host's OS, architecture, CPUs, tool versions and timings, never its address. A host that cannot be reached, a missing result or a result for another commit is a FAIL, never a pass. The output lines prefixed `linux|` are the host's; each job's full log stays in its run directory under `target/ci-remote/`. -- **Sharing the host.** At most four runs use the host at once (`--linux-slots N` or `OCTOSENSE_CI_LINUX_SLOTS`), through mkdir locks in `~/octosense-ci/locks/` on the host, separate from the Mac's two slots. A run's Linux jobs run in parallel, each with its own cargo target in `~/octosense-ci/cache/target/slot-/` (a warm run reuses the slot's built dependencies; the workspace's own crates and `.sources/` rebuild, since cargo keys them by the run directory's path) and `CARGO_BUILD_JOBS` of the host's CPUs divided by the slots (`--linux-jobs N`). The octos kernel cache is `~/octosense-ci/cache/octos-kernel/`. -- **Security.** The host runs code from pull-request branches of a public repository, as any CI does. Everything runs as the ssh user in `~/octosense-ci/`, never with sudo. The remote steps get an allow-listed environment (`HOME`, `USER`, `LOGNAME`, `LANG`, a fixed `PATH`, and the runner's own variables): no tokens and nothing from the Mac's environment. Never copy credentials there. The host's address and the key path are kept out of the log and `last.json`. -- **One-time setup on the host** (as the ssh user; no sudo): rustup with the stable toolchain and clippy and rustfmt, `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --profile minimal -c clippy -c rustfmt`, and a Python venv at `~/octosense-ci/venv` (`python3 -m venv --without-pip ~/octosense-ci/venv` when the distribution's `python3-venv` is not installed; the steps need no Python packages). The hub clones are made on the first run. Building the shell (the sandbox tests) needs Makepad's Linux build dependencies, which the host administrator installs (`pkg-config`, `clang`, `libx11-dev`, `libxcursor-dev`, `libxkbcommon-dev`, `libwayland-dev`, `libvulkan-dev`, `libasound2-dev`, `libpulse-dev`, `libgl-dev`). Steps that need more than that (a JDK for rom's product tests, the Android SDK, node for the web installer) are SKIPPED there with the reason, as on the Mac. +**What runs there.** The runner ships the commit under test (`HEAD`, without uncommitted changes) to `~/octosense-ci/repo.git` as an incremental git bundle and checks it out in `~/octosense-ci/runs/-`, keeping the newest five. The host runs `tools/setup.py` from the same lock files, then that checkout's own `tools/ci_local.py`, so the same `run:` steps run. + +**Results.** Remote steps appear in the same table, marked `(linux)`, and in `last.json` with the `"sha"` the host checked out. An unreachable host, a missing result or a result for another commit is a FAIL. Each job's full log stays in its run directory on the host (`target/ci-remote/`). + +**Sharing the host.** At most four runs use the host at once (`--linux-slots N`), through locks in `~/octosense-ci/locks/`. A run's Linux jobs run in parallel, each with its own cargo target and `CARGO_BUILD_JOBS` of the host's CPUs divided by the slots (or `--linux-jobs N`). + +**Security.** The host runs code from pull-request branches. Everything runs as the ssh user in `~/octosense-ci/`, never with sudo, and remote steps get an allow-listed environment (`HOME`, `USER`, `LOGNAME`, `LANG`, a fixed `PATH` and the runner's own variables): no tokens and nothing from the Mac. Never copy credentials there. The log and `last.json` show `` in place of the host's address and key path. + +**One-time setup on the host**, as the ssh user and without sudo: rustup with the stable toolchain, clippy and rustfmt, and a Python venv at `~/octosense-ci/venv`, first on the remote `PATH`. The steps need no Python packages, so `--without-pip` is enough, even without the distribution's `python3-venv`. + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --profile minimal -c clippy -c rustfmt +python3 -m venv --without-pip ~/octosense-ci/venv +``` + +The first run clones the source hubs into `~/octosense-ci/cache/hub/`. Building the shell for the sandbox tests needs Makepad's [Linux build dependencies](https://github.com/OctoSense-org/makepad#linux-dependencies), which the host's administrator installs. Steps that need a JDK, the Android SDK or node are SKIPPED there too. ## Merge on a local pass @@ -44,13 +67,19 @@ tools/ci-local.sh --only all # on that exact head, clean tree tools/ci-local-merge.sh # --dry-run to preview ``` -`tools/ci-local-merge.sh` refuses unless `target/ci-local/last.json`: +`tools/ci-local-merge.sh` merges only an open, non-draft pull request against `main`, and only if `target/ci-local/last.json`: -- passed on the PR's exact head commit, with a clean tree; +- ran on the PR's exact head commit, with a clean tree; - comes from a head that contains the current `origin/main`; -- covers every workflow GitHub would run for the PR's files (their `pull_request` `paths`), with no FAIL, no NOT RUN and no unexpected SKIP in them. A workflow GitHub would not run for the PR does not block it, so `--only desktop,phone` is enough evidence for a PR that only triggers those two. -- for steps run with `--linux-host`: ran on the Linux host on that same head commit. A remote result bound to any other commit is stale and refused. The Linux-only checks (`linux-host / sandbox`) count like the workflows they cover: they block a PR that triggers desktop, phone or apps. +- covers each workflow GitHub would run for the PR's files (their `pull_request` `paths`), with no FAIL, NOT RUN or SKIPPED step in them or in the drift check. Other workflows don't count, so `--only desktop,phone` is enough for a PR that triggers only those two; +- binds every `--linux-host` step to that same commit; a remote result for another commit is stale. The `linux-host / sandbox` checks count like the workflows they cover when present, but a run without them is not refused. + +It also refuses while the newest finished push run of a workflow on `main` has failed (cancelled runs don't count). Pass `--fixes-main` only for the PR that fixes it. Then it posts the summary table as a PR comment ("Local CI passed on `` …") and runs `gh pr merge --admin --merge --match-head-commit `, with the subject `Merge pull request # from /`. `--dry-run` prints the comment instead of posting it, and merges nothing. + +On `main`, pushes share one concurrency group per workflow (`desktop-main`, `phone-main`, `apps-main`) that cancels older runs, so the merges don't pile up and only the newest commit's run completes. `rom.yml`, which few changes trigger, has no group. -It also refuses while the latest completed GitHub run of a workflow on `main` has failed. Pass `--fixes-main` only for the PR that fixes it. Once every check passes, it posts the summary table as a PR comment ("Local CI passed on `` …") and runs `gh pr merge --admin --merge --match-head-commit `, with the subject `Merge pull request # from /`. +## Reading the code -The merge commit is an ordinary push to `main`, so GitHub CI runs on it. The runs don't pile up: pushes to `main` share one concurrency group per workflow (`desktop-main`, `phone-main`, `apps-main`; rom.yml runs only on rom changes and has no group) with `cancel-in-progress`. Only the newest `main` commit's run completes, and older queued or running `main` runs are cancelled. Pull-request runs keep their own per-PR groups, as before. A cancelled run doesn't count as red, but a failed one does: fix it before merging anything else. +- [`tools/ci_local.py`](../tools/ci_local.py): the runner. `ACTIONS` maps GitHub actions to local stand-ins, `JOBS` lists every job the runner knows, `STEP_REQUIREMENTS` what a step needs (a JDK, the Android SDK), `NOT_LOCAL` the workflows left out, and `LINUX_HOST_JOBS` the extra Linux checks. +- [`tools/ci_local_merge.py`](../tools/ci_local_merge.py): the merge's checks (`evidence_problems`, `red_main`). +- [`tools/test_ci_local.py`](../tools/test_ci_local.py): the tests of both, run in the desktop job. diff --git a/docs/mail-agent-events.md b/docs/mail-agent-events.md index 9a1887ff..36dc2861 100644 --- a/docs/mail-agent-events.md +++ b/docs/mail-agent-events.md @@ -2,142 +2,98 @@ English | [简体中文](mail-agent-events.zh-CN.md) -Mail can process new Inbox messages without a person sending a chat prompt for -each message. A person first connects an account on the host-owned sign-in -sheet, allows Mail's agent, and asks the system agent to configure the automation. -The system agent calls `agents.provision` with instructions, named skill text, -an enabled flag and a polling interval. `agents.status` reports the current -configuration and processing state. Provider credentials and mail passwords -stay with the host; message text read by the agent goes to the configured model. +Mail's agent handles new mail without being asked. A worker in the shell syncs the Inbox, queues each new message as an event and starts a turn of Mail's agent, which reads the message and either posts a glance card, with a reply draft if the person's policy asks for one, or records why not. [Key concepts](../README.md#key-concepts) explains agents, lanes and cards. + +## Turning it on + +The person signs in to Mail on the shell's sheet, allows Mail's agent and asks the system agent to turn on new-mail processing. The system agent calls `agents.provision` with `enabled`, `instructions`, named `skills` text and an optional `poll_interval_secs` (30 to 3600 seconds, default 60). It cannot name an account: the shell binds the provision to the signed-in one and keeps it outside the agent's folder. Its instructions are appended to Mail's admitted `AGENT.md`, and a skill named like an admitted one replaces that skill's text; neither grants a tool. `enabled: false` turns processing off, and `agents.status` reports the settings, the queue and the last outcome. + +Mail passwords and provider keys stay with the shell, but the mail the agent reads goes to the person's model provider. ```mermaid sequenceDiagram participant Person participant System as System agent - participant Host as Mail host and dispatcher - participant Mail as Mail app agent + participant Host as Shell: Mail service and worker + participant Mail as Mail's agent participant UI as Glance and notification - Person->>Host: Sign in and allow Mail's agent - Person->>System: Configure new-email automation - System->>Host: agents.provision(instructions, skills, enabled) - Host->>Host: Sync Inbox; establish initial baseline - loop New mail after baseline - Host->>Host: Atomically save messages, cursor and pending event - Host->>Mail: Incoming event with message IDs + Person->>Host: Sign in, allow Mail's agent + Host->>Host: The first Inbox sync sets a baseline + Person->>System: Turn on new-mail processing + System->>Host: agents.provision(enabled, instructions, skills) + loop Each new Inbox message + Host->>Host: Save messages, cursor and event atomically + Host->>Mail: Turn: event_id, message, sender, subject Mail->>Host: mail.peek(message) - Host-->>Mail: Bounded plain-text message - Mail->>Mail: Decide whether attention is needed - opt A card is useful - Mail->>Host: mail.publish_card(source, data, event ID) - Host->>UI: Validate L0, publish as Mail, notify + Host-->>Mail: Plain text, in pages + Mail->>Mail: Decide whether to tell the person + alt A card helps + opt The policy asks for a reply + Mail->>Host: mail.propose_reply(message, body) + Host-->>Mail: draft_id, revision, chat_thread + end + Mail->>Host: mail.publish_card(card_id = event_id, source, data, draft_id) + Host->>UI: Check L0, data and draft binding, publish as Mail, notify + else No card needed + Mail->>Host: mail.skip_event(event_id, reason) end - opt No card needed - Mail->>Host: mail.skip_event(event ID, reason) - end - Mail-->>Host: Complete turn - Host->>Host: Check publication/skip receipt, then acknowledge + Mail-->>Host: Turn completes + Host->>Host: Acknowledge if a receipt exists end ``` -The worker polls only the currently active, consented account with an enabled -provision. First sync and server UID resets establish a baseline without turning -old mail into notifications. Pending events survive restart. Failed turns remain -pending and retry with backoff. Acknowledgement requires both a successful turn -and a durable host receipt for publication or an explicit `mail.skip_event` -decision (`no_action`, `duplicate` or `outside_policy`). A final answer after a -failed tool call does not satisfy this check. - -Read the implementation in this order: - -1. [`incoming.rs`](../apps/mail/host-service/src/incoming.rs) serializes UI and - background sync. The cursor, messages and pending event queue share an atomic - mailbox save. Queue backpressure prevents advancing past undelivered events. -2. [`agent_events.rs`](../crates/shell/src/agent_events.rs) persists account-bound - provisions outside the app workspace, starts incoming turns and checks - cancellation, consent and account changes while waiting for completion. - Status uses a nonblocking queue snapshot (`pending: null`, `queue_busy: true` - during a sync). Completion checks the saved decision and acknowledges under - one nonblocking service lock; a busy queue retries without freezing the UI. -3. [`script_apps.rs`](../crates/shell/src/host_tools/script_apps.rs) loads admitted - `AGENT.md`/skill text and binds every Mail tool call to the broker's account. - A model cannot choose another account through tool arguments. `mail.folders` - and `mail.list` read caches; `mail.sync` refreshes a folder in bounded batches; - `mail.peek` reads plain text without marking mail read. Neither the password - vault nor the mailbox directory is mounted into the peer workspace. -4. [`guidance.rs`](../crates/app-peers/src/guidance.rs) snapshots host-provisioned - instruction and skill **text** on each turn, including existing peers. The - guidance and untrusted request occupy separate serialized text blocks. This - does not create a kernel system-message role or install native skills; tool - grants and account checks remain the enforceable authority boundary. -5. [`glance.rs`](../crates/shell/src/glance.rs) validates generated L0 source and - named dataset objects before publishing with Mail's attribution. Each dataset - requires a nonempty fields list and values for those fields; cyclic source - dependencies are rejected. This detects missing bindings, not factual accuracy - or visual quality. The model uses the stable incoming event - ID as `card_id`. A recorded publication receipt suppresses repeat publication; - a crash between publication and receipt persistence can still repeat a - notification. The stable card ID replaces the card rather than adding another. - Glance cards themselves are currently in memory; these receipts do not restore - visible cards after a process restart. -6. [`mobile_app.rs`](../crates/shell/src/mobile_app.rs) remembers the exact card - key behind each notification. A tap opens that full card over Glance; Back or - the sheet's close button returns to Glance. A removed card falls back safely. - The card's separate app-opening action still opens Mail. - -An actionable email also needs usable controls inside its generated card. -Mail's admitted skill now requests declarative L0 `Chip` buttons, named events -and local view state: for example, Delivery details or Appointment details, -then Back. The button must reveal facts from that email and accurately name -its effect. A suggestion in a text paragraph, the shell's open-Mail control, -or a local state called “sent” does not implement an email action. The existing -[shipping](../crates/shell/resources/glance/mail-shipping.card) and -[request](../crates/shell/resources/glance/mail-request.card) fixtures demonstrate -the interaction syntax; their demo send/tracking/done states must not be -presented as real remote operations. Sending, booking and mailbox mutations -from generated cards remain outside this implementation. The earlier device -trial below established publication and opening, not in-card button behavior. -The subsequent [paired button test](testing/mail-card-actions-2026-10-04.md) -verified Show code → Back → Details → Back for both models in the phone's full -card and Glance, after preserving earlier failed layouts. - -That shared scenario uses a pickup email with a code and no tracking URL: -Show code and Details reveal the supplied code, location, -deadline and photo-ID requirement, with Back from both views. These local -controls address a supported part of the -[email action-card plan](../apps/mail/docs/2026-10-01-email-action-card-plan.md). -The plan's external Track action remains separate work: although L0 catalogues -`sys.link`, this shell does not execute its writes. A missing URL must never -be replaced with an invented link or a fake tracking-success screen. - -The current shell has a rendering limitation: an accepted `Chip(width: .fill)` -can collapse inside its Fit wrappers and become invisible. The admitted skill -therefore requests natural-width Chips, stacked vertically, with short labels. -This is a renderer limitation, not an invalid L0 token. The -[button follow-up](testing/mail-card-actions-2026-10-04.md) separates failed -layouts, actual phone taps and the remaining remote-action scope. - -The Mail window can be closed during processing, but the OctoSense process must -remain alive. This implementation has no Android JobScheduler/WorkManager, -foreground service or Android NotificationManager integration. Notifications -appear in OctoSense's own shade, and cards appear on its Glance screen. Do not -promise delivery after Android suspends or kills the process. - -Only Mail's `mail.messages.new` trigger is implemented here. Generic cron, -arbitrary app event routing, model selection per app, native skill discovery and -the remaining ADR 0002 budget/Settings UI are separate work. The system agent's -provision supplements admitted app guidance; email content cannot provision an -agent or expand its tools. Disabling the provision or revoking agent access -stops new turns and cancels the dispatcher's active context. - -Verification: on the assigned OnePlus 6, AgentMail messages delivered through -Gmail triggered real DeepSeek and MiniMax Mail turns without a per-email prompt. -Both models explicitly skipped the routine newsletter. DeepSeek's revised -shipping and appointment cards were readable; MiniMax's first pair had missing -data bindings despite successful publication. That failure led to the generated -data gate and model feedback. Its shipping retry displayed complete facts and -opened from the notification. After sender verification, a fresh MiniMax -appointment published on its first attempt and passed the phone notification, -full-card and Android Back checks. See the [recorded test](testing/mail-events-2026-10-04.md) -for build boundaries, source artifacts and limitations. The trial policy handles -only subjects prefixed `[OctoSense simulation]`; it skips other incoming messages -without reading their bodies. +## From new mail to a turn + +The worker is one thread in the shell process. The first sync of an account's Inbox only sets a baseline and queues nothing, as does a sync after an IMAP server renumbers the folder. After that, every Inbox sync (the worker's, the Mail window's or the agent's `mail.sync`) turns each new message into a pending event with a stable id. Messages, server cursor and events are saved in one atomic write and survive restarts. + +Each cycle takes the oldest event, syncing first when none is waiting. Its turn runs in the person's lane as the app's own (trigger `incoming`) and has 180 seconds. The worker then waits the poll interval or, after a failure, 30 seconds, doubling up to 15 minutes. + +## What the agent decides + +The turn carries the event's ids, sender and subject as untrusted JSON, with Mail's guidance in a separate block. The triage skill has the agent read the message and settle the event: + +| Tool | Use | +| --- | --- | +| `mail.peek` | Reads the message as plain text, up to 2,000 bytes a page, without marking it read. | +| `mail.propose_reply` | If the policy asks for a reply: creates or reuses a draft for the email, addressed by the host, and returns its `draft_id`. | +| `mail.publish_card` | Publishes a card the model wrote, with the event id as `card_id` and, for a reply, the `draft_id`. | +| `mail.notify` | The fallback when no valid card can be made: a plain notice, same `card_id`. | +| `mail.skip_event` | Records `no_action`, `duplicate` or `outside_policy`. | + +The shell binds every Mail tool call to the signed-in account; that binding and the tool grants, not the guidance, limit the agent. No agent tool sends mail: `mail.propose_send` only prepares a proposal, and only the host's review, approved by a physical touch on an Android phone, authorizes SMTP, even in developer mode or under a standing rule. + +## When an event is done + +The worker acknowledges an event, removing it from the queue, only if the turn completed, the agent is still allowed for the same account, the provision is unchanged, and Mail's host service holds a receipt for that event id: a publication or a skip. Creating a draft is not one. A final answer after a failed tool call leaves no receipt, so the event is retried. Turning processing off or withdrawing the agent closes the running turn within 250 ms and leaves its event pending. + +Delivery is at least once: a failure between publishing and saving the receipt repeats the notification, but the same card id replaces any card still shown. An identical retry reuses the receipt silently; a corrected card republishes under the same id. + +## Cards + +A card from `mail.publish_card` is L0 only, with no expressions or script. The shell checks that each `sys.dataset` source declares a nonempty `fields` list with a value in `data` for every field, and that sources form no cycle. The check catches missing bindings, not wrong facts. The shell then publishes the card as Mail and notifies; tapping the notification opens that exact card, which the phone shows as a full-screen workspace. + +With a `draft_id`, Mail's host service binds the card to the account, email, draft and chat thread, a binding the model can neither supply nor change. The card can then edit the reply (`sys.mail_draft`), chat about it (`sys.chat`) and open the host's review (`sys.mail_review`). [Composed Mail cards](mail-composable-cards.md#follow-one-reply-through-the-code) follows a reply from draft to SMTP. + +Other mail that needs action gets working `Chip` buttons that switch local views (`state`, `event`, `when`), such as Show code, Details and Back. They only change what the card shows, and their labels must not claim to send, book or track. [`mail-shipping.card`](../crates/shell/resources/glance/mail-shipping.card) and [`mail-request.card`](../crates/shell/resources/glance/mail-request.card) show the syntax; their send, track and done states are demos. + +## Limits + +- The Mail window may be closed, but nothing runs while Android suspends or kills OctoSense, and notifications appear in OctoSense's own shade, not Android's. +- Glance cards live in memory, so a restart removes them, except draft-bound Mail cards, which the shell restores for the signed-in account without a new notification. +- Mail keeps at most four live cards. A fifth card or notice is refused, and unless the agent skips that event, it stays first in the queue until a card is dismissed or expires (after 24 hours by default). +- Events queue even while processing is off, and only the worker removes them. Once 128 are waiting, every Inbox sync that finds new mail fails without moving the cursor, the Mail window's included, until processing drains the queue. +- Not yet: events for other apps, scheduled triggers, per-app model choice, kernel-native skills, the budget and Settings UI of [ADR 0002](adr/0002-event-driven-app-agents.md), and remote actions other than replying (the shell has no `sys.link` handler). + +## Reading the code + +1. [`incoming.rs`](../apps/mail/host-service/src/incoming.rs): the queue (`collect`, `skip_event`, `resolve_and_ack_try`) and the publication receipts (`publish_revision`, `publish_once`). +2. [`agent_events.rs`](../crates/shell/src/agent_events.rs): `provision`, `status` and the worker (`poll`, `deliver`). +3. [`script_apps.rs`](../crates/shell/src/host_tools/script_apps.rs): admitted guidance (`guidance`) and account binding (`scoped_args`). +4. [`guidance.rs`](../crates/app-peers/src/guidance.rs): the per-turn guidance, at most 16 KiB. +5. [`glance.rs`](../crates/shell/src/glance.rs): `publish_mail_l0_for` and `check_generated_data`. +6. [`mail_card.rs`](../crates/shell/src/mail_card.rs): a bound card's sources (`check_sources`); its `persistence.rs` restores bound cards. +7. [`mobile_app.rs`](../crates/shell/src/mobile_app.rs): notification taps on the phone. + +## How it was tested + +On a OnePlus 6, with DeepSeek and MiniMax: the [new-mail test](testing/mail-events-2026-10-04.md) and the [card-button test](testing/mail-card-actions-2026-10-04.md). [Composed Mail cards](mail-composable-cards.md) records the reply-card checks. diff --git a/docs/mail-agent-events.zh-CN.md b/docs/mail-agent-events.zh-CN.md index 8a383eec..66f683db 100644 --- a/docs/mail-agent-events.zh-CN.md +++ b/docs/mail-agent-events.zh-CN.md @@ -2,80 +2,98 @@ [English](mail-agent-events.md) | 简体中文 -Mail 可以处理 Inbox 新邮件,不需要用户逐封发送聊天请求。用户先在宿主登录面板中连接账户, -允许 Mail Agent,再让系统 Agent 配置自动处理。系统 Agent 用 `agents.provision` -设置指令、具名技能文本、启用状态和轮询间隔;`agents.status` 返回配置与处理状态。 -模型密钥和邮箱密码留在宿主中;Agent 读取的邮件文本会发送给所配置的模型。 - -流程是:宿主同步 Inbox → 原子保存邮件、同步游标和待处理事件 → Mail Agent 收到带邮件 ID -的 incoming 事件 → 用 `mail.peek` 读取正文 → 自行判断是否需要卡片 → 用 -`mail.publish_card` 发布模型编写的 L0 卡片和通知 → 回合成功且宿主有发布记录后确认事件。 -无需卡片时必须调用 `mail.skip_event`,原因是 `no_action`、`duplicate` 或 `outside_policy`; -宿主保存决策,回合成功后才确认。工具失败后的普通模型回答不能满足确认条件。每封邮件不需要系统 Agent 或测试人员另发提示词。 - -轮询只作用于当前已获用户许可、且配置已启用的账户。首次同步或服务器 UID 重置只建立基线, -不会将历史邮件转成一批通知。待处理事件持久保存;失败或缺少宿主决策记录的回合保留并退避重试。 - -按以下顺序阅读源码: - -1. [`incoming.rs`](../apps/mail/host-service/src/incoming.rs) 串行处理 UI 和后台同步。 - 邮件、游标和事件队列一次原子保存;队列满时不会推进游标丢失事件。 -2. [`agent_events.rs`](../crates/shell/src/agent_events.rs) 在应用工作区之外保存绑定账户的 - 配置,启动 incoming 回合,并在等待期间检查取消、授权撤销和账户变化。 - 状态读取不会等待同步锁;队列繁忙时返回 `pending: null`、`queue_busy: true`。 - 完成处理在同一次非阻塞服务锁中检查已保存的决策并确认事件;繁忙时重试,不冻结 UI。 -3. [`script_apps.rs`](../crates/shell/src/host_tools/script_apps.rs) 加载已接纳的 - `AGENT.md`/技能文本,将 Mail 工具调用绑定到 broker 提供的账户。模型不能用参数选择 - 另一个账户。`mail.folders`、`mail.list` 读缓存;`mail.sync` 分批刷新指定文件夹; - `mail.peek` 读取纯文本且不标记已读。Peer 工作区不会挂载密码保险库或邮箱数据库。 -4. [`guidance.rs`](../crates/app-peers/src/guidance.rs) 为每轮快照宿主指令与技能**文本**, - 已存在的 peer 下一轮也会用新配置。指令和不可信请求放在两个独立序列化文本块中。 - 这没有增加内核 system 消息角色,也没有安装原生技能;工具授权和账户检查才是可强制执行的边界。 -5. [`glance.rs`](../crates/shell/src/glance.rs) 校验 L0 源码及具名数据集对象,以 Mail 身份发布。 - 每个数据集必须声明非空字段列表并提供各字段的值;循环依赖会被拒绝。 - 这能发现缺失绑定,但不能证明事实正确或布局好看。 - 模型用稳定事件 ID 作为 `card_id`;已记录的发布结果阻止重复发布。若在发布后、记录前崩溃, - 通知仍可能重复,但相同卡片 ID 会替换卡片而非新增另一张。Glance 卡片本身目前只保存在内存, - 这些记录不会在进程重启后恢复可见卡片。 -6. [`mobile_app.rs`](../crates/shell/src/mobile_app.rs) 保存每条通知对应的准确卡片键。 - 点击通知会在 Glance 上打开该完整卡片;返回键或关闭按钮回到 Glance。 - 卡片已移除时安全回退。卡片中独立的打开应用操作仍进入 Mail。 - -需要用户处理的邮件,生成卡片内部也应有可用控件。Mail 已接纳的技能现在要求使用声明式 -L0 `Chip` 按钮、具名事件和本地视图状态,例如“配送详情”或“预约详情”,以及“返回”。 -按钮必须展示该邮件中的事实,名称必须准确说明实际效果。正文里的建议、shell 的打开 -Mail 控件,或名为“已发送”的本地状态,都不等于实现了邮件操作。 -[物流](../crates/shell/resources/glance/mail-shipping.card)和 -[请求](../crates/shell/resources/glance/mail-request.card)示例展示交互语法;其中模拟的 -发送、追踪和完成状态不能冒充真实远端操作。从生成卡片发送邮件、预约或修改邮箱仍未实现。 -下述较早设备试验验证了发布和打开,没有验证卡片内按钮。后续 -[配对按钮试验](testing/mail-card-actions-2026-10-04.zh-CN.md)保留较早布局失败, -并实际验证两个模型在完整卡片及 Glance 上的取件码→返回→详情→返回。 - -该共同场景是有取件码、无追踪网址的邮件:“显示取件码”和“取件详情”必须展示 -邮件提供的码、地点、截止时间及携带带照片证件的要求,两种视图都能返回。这些本地控件 -实现了[邮件操作卡片计划](../apps/mail/docs/2026-10-01-email-action-card-plan.md)中 -当前可支持的部分。计划中的外部 Track 操作仍需另做宿主集成:L0 虽然收录了 `sys.link`, -本 shell 并不执行其写操作。没有网址时不能编造链接或显示虚假的追踪成功页面。 - -当前 shell 有渲染限制:已通过校验的 `Chip(width: .fill)` 可能在 Fit 包装内收缩到不可见。 -因此已接纳技能要求省略 width,使用自然宽度、纵向排列及短标签。这是渲染器限制, -不是非法 L0 token。[按钮后续试验](testing/mail-card-actions-2026-10-04.zh-CN.md) -分别记录布局失败、实际手机点击及尚未实现的远端操作范围。 - -Mail 窗口可以关闭,但 OctoSense 进程必须存活。当前没有 Android JobScheduler/WorkManager、 -前台服务或 Android NotificationManager 集成。通知显示在 OctoSense 自己的通知栏, -卡片显示在 Glance。不能承诺 Android 挂起或终止进程后仍会送达。 - -这里只实现 Mail 的 `mail.messages.new`。通用 cron、任意应用事件路由、每应用模型选择、 -内核原生技能发现,以及 ADR 0002 剩余预算和 Settings 界面仍是后续工作。系统 Agent 的配置 -补充应用已接纳的指令;邮件内容不能配置 Agent 或扩大工具权限。关闭配置或撤销 Agent -访问权会停止新回合并取消调度器的活动上下文。 - -验证:指定 OnePlus 6 收到 AgentMail 经 Gmail 投递的邮件后,确实自动触发了 DeepSeek -和 MiniMax 的 Mail 回合,没有逐封人工提示。两个模型均明确跳过普通简报。 -DeepSeek 改进后的物流、预约卡片可读;MiniMax 第一组虽然发布成功,但数据绑定缺失, -这促成了生成数据校验和模型反馈。其物流重试已经完整显示事实,并可由通知打开; -发件账户验证后,新的 MiniMax 预约卡片首次发布成功,通知、完整卡片及 Android 返回键实测均通过。构建范围、原始生成源码和限制见 -[测试记录](testing/mail-events-2026-10-04.zh-CN.md)。试验策略仅处理主题以 -`[OctoSense simulation]` 开头的邮件;其他新邮件不读正文,直接跳过。 +邮件的 Agent 不用别人开口就能处理新邮件。Shell 中的一个工作线程同步收件箱,把每封新邮件记为一个事件排入队列,并为它启动邮件的 Agent 的一个回合;Agent 读完邮件,要么发布一张 glance 卡片(如果用户的策略要求回复,卡片会带一份回复草稿),要么记下不发的理由。Agent、通道和卡片的概念见[关键概念](../README.zh-CN.md#关键概念)。 + +## 开启 + +用户在 Shell 的面板上登录邮件账号、允许邮件的 Agent,再请系统 Agent 打开新邮件处理。系统 Agent 随即调用 `agents.provision`,参数为 `enabled`、`instructions`、具名的 `skills` 文本,以及可选的 `poll_interval_secs`(30 到 3600 秒,默认 60)。它不能指定账号:Shell 把这份配置绑定到当前登录的账号,并保存在 Agent 的文件夹之外。其中的 instructions 追加在邮件应用已接纳的 `AGENT.md` 之后;与已接纳技能同名的技能会替换那项技能的文本。两者都不授予任何工具。`enabled: false` 关闭处理,`agents.status` 报告配置、队列和最近一次的结果。 + +邮箱密码和模型提供方的密钥都留在 Shell 一侧,但 Agent 读到的邮件会发送给用户配置的模型提供方。 + +```mermaid +sequenceDiagram + participant Person as 用户 + participant System as 系统 Agent + participant Host as Shell 中的邮件服务与工作线程 + participant Mail as 邮件的 Agent + participant UI as glance 卡片与通知 + Person->>Host: 登录,允许邮件的 Agent + Host->>Host: 收件箱首次同步只建立基线 + Person->>System: 打开新邮件处理 + System->>Host: agents.provision(enabled, instructions, skills) + loop 每封新到的收件箱邮件 + Host->>Host: 原子保存邮件、游标和事件 + Host->>Mail: 回合:event_id、message、sender、subject + Mail->>Host: mail.peek(message) + Host-->>Mail: 纯文本,分页返回 + Mail->>Mail: 判断要不要告诉用户 + alt 卡片有用 + opt 策略要求回复 + Mail->>Host: mail.propose_reply(message, body) + Host-->>Mail: draft_id、revision、chat_thread + end + Mail->>Host: mail.publish_card(card_id = event_id, source, data, draft_id) + Host->>UI: 检查 L0、数据和草稿绑定,以邮件应用身份发布并通知 + else 不需要卡片 + Mail->>Host: mail.skip_event(event_id, reason) + end + Mail-->>Host: 回合完成 + Host->>Host: 有回执才确认事件 + end +``` + +## 从新邮件到回合 + +工作线程只有一个,运行在 Shell 进程中。某个账号的收件箱第一次同步时只建立基线,不排入任何事件;IMAP 服务器给文件夹重新编号之后的那次同步也是如此。此后,每一次收件箱同步(工作线程的、邮件窗口的,或 Agent 自己调用的 `mail.sync`)都会把每封新邮件变成一个待处理事件,事件 id 固定不变。邮件、服务器游标和事件在一次原子写入中保存,重启后依然保留。 + +每个周期只处理一个事件,即最早的那个;没有待处理事件时先同步一次。它的回合在用户的通道中运行,算作应用自己的回合(触发方式为 `incoming`),限时 180 秒。之后工作线程等待一个轮询间隔;如果失败,则先等 30 秒,每次翻倍,最长 15 分钟。 + +## Agent 如何决定 + +回合以不可信 JSON 的形式带来事件的各个 id、发件人和主题,邮件应用的指导文本则放在另一个独立的块中。Agent 按照分拣技能读取邮件,然后用下面的工具了结这个事件: + +| 工具 | 用途 | +| --- | --- | +| `mail.peek` | 以纯文本读取邮件,每页最多 2,000 字节,不会把邮件标为已读。 | +| `mail.propose_reply` | 如果策略要求回复:为这封邮件新建或复用一份草稿,收件人由宿主填写,并返回它的 `draft_id`。 | +| `mail.publish_card` | 发布模型编写的卡片,以事件 id 作为 `card_id`;如果是回复,再附上 `draft_id`。 | +| `mail.notify` | 生成不了有效卡片时的后备:一条普通通知,`card_id` 相同。 | +| `mail.skip_event` | 记录 `no_action`、`duplicate` 或 `outside_policy`。 | + +Shell 把邮件应用的每一次工具调用都绑定到当前登录的账号;限制 Agent 的是这一绑定和工具授权,而不是指导文本。没有任何 Agent 工具能发送邮件:`mail.propose_send` 只准备一份发送提议,只有宿主的审核在 Android 手机上经实体触摸批准,才能授权 SMTP 发送;开发者模式或常设规则也绕不过去。 + +## 事件何时算处理完 + +只有在回合已经完成、Agent 对同一账号仍然获准、配置没有变化,并且邮件应用的宿主服务持有该事件 id 的回执(一次发布或一次跳过)时,工作线程才确认事件,把它移出队列。创建草稿不算回执。工具调用失败后给出的最终回答不会留下回执,所以事件会被重试。关闭处理或撤回对 Agent 的允许,会在 250 毫秒内关闭正在运行的回合,它的事件仍保持待处理。 + +投递至少一次:发布之后、保存回执之前出错,通知会重复,但相同的卡片 id 会替换仍在显示的卡片。完全相同的重试会沿用回执,不再通知;修正过的卡片会以同一个 id 重新发布。 + +## 卡片 + +`mail.publish_card` 发布的卡片只能是 L0:没有表达式,也没有脚本。Shell 会检查每个 `sys.dataset` 源都声明了非空的 `fields` 列表,并在 `data` 中为每个字段提供了值,还会检查各个源之间没有循环依赖。这项检查能发现缺失的绑定,发现不了错误的事实。之后 Shell 以邮件应用的身份发布卡片并发出通知;点按通知打开的正是这张卡片,手机上以全屏工作区显示。 + +带 `draft_id` 时,邮件应用的宿主服务把卡片绑定到账号、邮件、草稿和聊天线程,这个绑定,模型既不能提供,也不能更改。卡片随后可以编辑回复(`sys.mail_draft`)、就回复聊天(`sys.chat`),并打开宿主的审核(`sys.mail_review`)。一封回复从草稿到 SMTP 的路径见[组合 Mail 卡片](mail-composable-cards.zh-CN.md#沿代码追踪一封回复)。 + +其他需要用户处理的邮件,会得到能用的 `Chip` 按钮,在本地视图之间切换(`state`、`event`、`when`),例如 Show code、Details 和 Back。这些按钮只改变卡片显示的内容,名称也不能声称会发送、预约或追踪。[`mail-shipping.card`](../crates/shell/resources/glance/mail-shipping.card) 和 [`mail-request.card`](../crates/shell/resources/glance/mail-request.card) 展示了语法;其中的发送、追踪和完成状态只是演示。 + +## 限制 + +- 邮件窗口可以关闭,但 Android 挂起或终止 OctoSense 期间什么都不会运行;通知显示在 OctoSense 自己的通知栏里,而不是 Android 的通知栏。 +- glance 卡片只保存在内存中,重启会清除它们;绑定草稿的邮件卡片例外,Shell 会为当前登录的账号恢复它们,且不再发出通知。 +- 邮件最多保留四张有效卡片。第五张卡片或通知会被拒绝;除非 Agent 跳过该事件,它会留在队首,直到有卡片被关闭或过期(默认 24 小时后)。 +- 关闭处理时事件仍会排队,而且只有工作线程会移除它们。积满 128 个后,凡是发现新邮件的收件箱同步都会失败且游标不会前移(包括邮件窗口的同步),直到处理消化掉队列。 +- 尚未实现:其他应用的事件、定时触发、按应用选择模型、内核原生技能、[ADR 0002(英文)](adr/0002-event-driven-app-agents.md)中的预算与设置界面,以及回复之外的远端操作(Shell 没有 `sys.link` 的处理程序)。 + +## 阅读源码 + +1. [`incoming.rs`](../apps/mail/host-service/src/incoming.rs):队列(`collect`、`skip_event`、`resolve_and_ack_try`)和发布回执(`publish_revision`、`publish_once`)。 +2. [`agent_events.rs`](../crates/shell/src/agent_events.rs):`provision`、`status` 和工作线程(`poll`、`deliver`)。 +3. [`script_apps.rs`](../crates/shell/src/host_tools/script_apps.rs):已接纳的指导文本(`guidance`)和账号绑定(`scoped_args`)。 +4. [`guidance.rs`](../crates/app-peers/src/guidance.rs):每个回合附带的指导文本,最多 16 KiB。 +5. [`glance.rs`](../crates/shell/src/glance.rs):`publish_mail_l0_for` 和 `check_generated_data`。 +6. [`mail_card.rs`](../crates/shell/src/mail_card.rs):绑定卡片的源(`check_sources`);其中的 `persistence.rs` 负责恢复绑定卡片。 +7. [`mobile_app.rs`](../crates/shell/src/mobile_app.rs):手机上的通知点按。 + +## 测试情况 + +在 OnePlus 6 上用 DeepSeek 和 MiniMax 测试:见[新邮件测试](testing/mail-events-2026-10-04.zh-CN.md)和[卡片按钮测试](testing/mail-card-actions-2026-10-04.zh-CN.md)。回复卡片的检查记录在[组合 Mail 卡片](mail-composable-cards.zh-CN.md)中。 diff --git a/docs/plans/2026-09-30-photos-memories.md b/docs/plans/2026-09-30-photos-memories.md index 7b298877..46f48bce 100644 --- a/docs/plans/2026-09-30-photos-memories.md +++ b/docs/plans/2026-09-30-photos-memories.md @@ -1,5 +1,7 @@ # Photos Memories Implementation Plan +**Status (2026-10-04):** Done: merged in [#266](https://github.com/OctoSense-org/OctoSense/pull/266) (a3098486), including f6ba06a6 (memory snapshots use the account data path) and a5f1a6e1 (Photos' agent stays read-only). Live providers and physical phones remain unverified. + **Goal:** Turn Photos' preset moment card into a browsable collection of saved AI-curated stories, optionally guided by a prompt. **Architecture:** Photos calls the existing host-owned `model.complete` service with bounded catalog metadata and a strict story schema. It validates membership and uniqueness of photo IDs, persists accepted stories separately from the existing album store, and keeps local moments available without an AI provider. No image bytes or credentials enter the request. diff --git a/docs/plans/2026-10-03-platform-app-icons.md b/docs/plans/2026-10-03-platform-app-icons.md index f358a754..46f77c14 100644 --- a/docs/plans/2026-10-03-platform-app-icons.md +++ b/docs/plans/2026-10-03-platform-app-icons.md @@ -1,5 +1,7 @@ # Platform app icons implementation plan +**Status (2026-10-04):** Done: merged in [#323](https://github.com/OctoSense-org/OctoSense/pull/323) (5a707de8); the Android installation follow-up is below. + **Goal:** Use OctoSense's existing eight-petal green mark in installed mobile apps and desktop builds. **Architecture:** Extend the deterministic, standard-library icon generator in `desktop/packaging/make_icons.py`. Keep desktop release artwork intact, generate assets at the paths the pinned Makepad packager reads, and overlay OpenHarmony's template icons in the product build wrapper. Commit generated assets so normal builds require no image tools. diff --git a/docs/testing/mail-card-actions-2026-10-04.md b/docs/testing/mail-card-actions-2026-10-04.md index 55a45abe..309f11f2 100644 --- a/docs/testing/mail-card-actions-2026-10-04.md +++ b/docs/testing/mail-card-actions-2026-10-04.md @@ -52,6 +52,8 @@ card was authored by its model through `mail.publish_card`; no tester wrote or patched the final source/data. Reviewers changed policy through the system agent and operated the UI. There was no per-email chat prompt or manual peer dispatch. Initial and intermediate receipts remain separate from final passes. +Each stage's APK also shipped revised bundled Mail guidance (`SKILL.md`; hashes in +the build receipts); Git keeps only the final 0409 version. | Policy stage / APK / pickup code | DeepSeek | MiniMax | | --- | --- | --- | diff --git a/docs/testing/mail-card-actions-2026-10-04.zh-CN.md b/docs/testing/mail-card-actions-2026-10-04.zh-CN.md index 63b00c5c..25a91ce8 100644 --- a/docs/testing/mail-card-actions-2026-10-04.zh-CN.md +++ b/docs/testing/mail-card-actions-2026-10-04.zh-CN.md @@ -43,6 +43,7 @@ Show code→Back→Details→Back 在完整面板及 Glance 上均可用。两 三个共同策略阶段共收到六封新邮件,产生六张卡片。每张都由模型通过 `mail.publish_card` 编写,没有测试人员手写或修补最终源码/数据。审查人员通过系统 Agent 调整策略并操作 UI, 没有逐封聊天提示或手动派发 peer 回合。初始、中间记录与最终通过结果分别保留。 +每个阶段的 APK 还带有修订后的内置 Mail 指引(`SKILL.md`,哈希见构建记录);Git 中只保留最终 0409 版本。 | 策略阶段 / APK / 取件码 | DeepSeek | MiniMax | | --- | --- | --- | diff --git a/docs/testing/mail-events-2026-10-04.zh-CN.md b/docs/testing/mail-events-2026-10-04.zh-CN.md index ae3cb73d..4de7e6ea 100644 --- a/docs/testing/mail-events-2026-10-04.zh-CN.md +++ b/docs/testing/mail-events-2026-10-04.zh-CN.md @@ -28,23 +28,23 @@ MiniMax 第一组 L0 语法有效,但数据放在顶层,而非 `data.note` 这些失败结果仍保留。宿主现在会拒绝缺失的数据集对象/字段、未声明字段列表及循环依赖, 让模型收到可自行修复的错误。 -[工具结果](mail-events-2026-10-04/tool-results.json) 同时保留失败和成功。 +[过滤后的工具结果](mail-events-2026-10-04/tool-results.json) 同时保留校验失败和成功。 [模型归属](mail-events-2026-10-04/provider-attribution.json) 来自各已完成回合的内核 -ledger 元数据,不依赖邮件主题或模型自述。[生成参数](mail-events-2026-10-04/cards/) +ledger 中的模型元数据,不依赖邮件主题或模型自述。[生成参数](mail-events-2026-10-04/cards/) 保留模型的原始源码字符串和数据值,没有人工修改。 ## 手机与原生 Makepad 审查 -手机 Glance 显示了改进后的 DeepSeek 卡片及修正后的 MiniMax 物流卡片。 +手机 Glance 显示了改进后的 DeepSeek 卡片;修正后的 MiniMax 物流卡片同时显示在 Glance 和完整卡片面板中。 点击后者的 Mail 通知打开了对应完整卡片。最终 MiniMax 预约在 `2026100406` 上也通过了 “通知→完整卡片→Android 返回键→Glance”,测试应用始终保持前台。较早构建的返回键失败, 原因是固定版本 Makepad 按 APK 包名查找扩展类。给独立包增加转发扩展后问题修复, -没有替换 Home 或修改 Makepad 版本。卡片关闭按钮另有单元测试,未实测其触摸点击。 +没有替换 Home 或修改 Makepad 版本。完整卡片面板的关闭按钮另有单元测试,未实测其触摸点击。 | 截图 | 证明范围 | | --- | --- | -| [DeepSeek Glance](mail-events-2026-10-04/screenshots/deepseek-compact-glance.png) | 完整预约事实、尚未预约状态及下一步 | -| [MiniMax 通知](mail-events-2026-10-04/screenshots/minimax-shipping-notification.png) | Mail 归属及物流通知 | +| [DeepSeek Glance](mail-events-2026-10-04/screenshots/deepseek-compact-glance.png) | 完整预约事实、“提议中、尚未预约”状态及下一步 | +| [MiniMax 通知](mail-events-2026-10-04/screenshots/minimax-shipping-notification.png) | Mail 归属及生成的物流通知 | | [MiniMax Glance](mail-events-2026-10-04/screenshots/minimax-shipping-glance.png) | 修正后的数据实际显示 | | [MiniMax 完整卡片](mail-events-2026-10-04/screenshots/minimax-shipping-full-card.png) | 点击通知后打开对应物流卡片 | | [最终预约](mail-events-2026-10-04/screenshots/minimax-appointment-full-card.png) | 最终构建的新邮件打开完整卡片 | @@ -57,7 +57,7 @@ ledger 元数据,不依赖邮件主题或模型自述。[生成参数](mail-ev 修正后的物流卡片显示了全部八个被引用字段。 原生工具的配色及字体与手机 shell 不同。自动 `fits` 可能遗漏部分文字被裁切的情况, -因此还比较了同宽高画布的 instrument 文字矩形,并查看实际手机截图。 +因此将同宽度下的 instrument 文字矩形与更高画布的渲染结果对比,并查看实际手机截图。 [原生测量](mail-events-2026-10-04/native-review.json) 记录了这些差异。 这是少量迭代案例,不能据此宣称通用速度或质量排名。本轮 DeepSeek 更符合紧凑信息层次; MiniMax 修正后的物流仍重复模拟免责声明,并用额外行分开标签和值。