diff --git a/docs/architecture.md b/docs/architecture.md index 4fcd0b0f..4f5679b1 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -10,7 +10,7 @@ Every statement carries its status: - **In progress**: an open pull request, linked. - **Planned**: decided in an ADR (linked, with its plan step) and not built yet. -The decisions behind it are [ADR 0001](adr/0001-one-octosense-repository.md) (one repository), [ADR 0002](adr/0002-event-driven-app-agents.md) (app agents, Proposed, as amended by 0004), [ADR 0003](adr/0003-shared-octos-client-access.md) (Talk to Octos) and [ADR 0004](adr/0004-native-apps-hosting-and-peers.md) (native apps, app agents, cross-app work and approvals, Accepted). The assistant's services, what each kind of app can call today and how to run them locally are in [ai-services.md](ai-services.md); this page does not repeat them. +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. ## Contents @@ -38,11 +38,13 @@ The decisions behind it are [ADR 0001](adr/0001-one-octosense-repository.md) (on flowchart LR person(["The person"]) subgraph shellp["OctoSense shell process (desktop, or Home on a phone)"] - wm["Window manager, launcher, sheets
crates/shell"] + wm["Window manager, launcher, sheets,
system chat, glance panel
crates/shell"] mods["In-process native modules
App Hub, Rinx, (AppCard)"] runner["App Hub Card runner
script apps in isolates"] router["Approval router
crates/shell/src/approvals"] aihost["ai-host + app-peers broker
host connection to the kernel"] + relay["Host-tool relay
crates/shell/src/host_tools"] + svc["App host services
mail, calendar, news; notices"] bus["AI services bus
crates/shell/src/ai_bus.rs"] end term["Process app (desktop)
Terminal"] @@ -60,6 +62,10 @@ flowchart LR aihost <-->|"OUP over stdio
(or host-token WebSocket)"| kern sys -- "peer_send_input / blackboard" --> peers ext -. "external token, allowlist" .-> sys + kern -- "peer/tool/call, approvals" --> relay + relay --> router + relay -- "app tools" --> svc + svc -- "glance.publish: cards" --> wm term -- "typed tools" --> bus mods -- "typed tools" --> bus bus --> router @@ -69,6 +75,7 @@ flowchart LR +--------------------------- 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 @@ -77,7 +84,7 @@ flowchart LR | process apps | | octos kernel (child; in process on OHOS)| | Terminal, ... | | system agent session | +-----------------+ | app peers: one per (app, account) | - | turns and tool dispatch | + | turns and tool dispatch | Talk to Octos clients ---------> +-----------------------------------------+ (external token, allowlist, opt-in) ``` @@ -119,14 +126,14 @@ Native apps are first-party Rust crates declared only in [`native-apps.json`](.. | 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 until the peer link and the process sandboxes exist (ADR 0004 §2). +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. -How the shell decides at run time (`crates/shell/src/apps.rs`, `AppRegistry::hosting`): if the target cannot run processes (`host::processes_available()` is false on wasm and native mobile), every app is a module; App Hub and Settings are always modules; otherwise a native app follows its manifest entry, and runs as a process only when a process form exists (a checkout to `cargo run` from, or a sibling binary). Release packages do not ship process apps' binaries yet ([#94](https://github.com/OctoSense-org/OctoSense/pull/94), in progress), so there they fall back to in-process. A per-app override (`~/.makepad/wm/apps.splash`, `--module `) can switch a module to a process. +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. **Process hosting** is Makepad's window-manager hosting (`crates/shell/src/clients.rs`, `hub.rs`): -- The shell starts the app as a child with `--stdin-loop` (`cargo run … -- --stdin-loop` in a checkout, else the sibling binary), in its own process group, with `STUDIO_HOST=http://127.0.0.1:` and its client id. -- The child connects back to the shell's **hub**, an HTTP/WebSocket server the shell binds on the first free loopback port in 8765–8785 (`WmHub::start`), and speaks Makepad's studio protocol (`AppToStudio` / `StudioToApp`). 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)). +- 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. @@ -134,7 +141,7 @@ How the shell decides at run time (`crates/shell/src/apps.rs`, `AppRegistry::hos ### Script apps -Script apps (the system apps News, Photos, Maps, Camera on phones, Mail, AI providers and YouTube, from `desktop/system-apps.json` and `phone/system-apps.json`, plus store apps) are OctoScript bundles. They all run in process, inside App Hub's **Card runner** (`CARD_MODULE` of `octosense-app-hub-app`): one nested isolate per app instance with `mod.res` and `mod.run` stripped, a jail and a quota. An app reaches the shell only through `host.request(".", …)` for the families its manifest was granted. A script bug fails inside its isolate. **On main.** +Script apps (the system apps News, Photos, Maps, Mail, AI providers and YouTube, plus Calendar on the desktop and 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.** ```mermaid flowchart TB @@ -160,25 +167,25 @@ 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 turns and tool dispatch in the session's workspace (OctoSense denies them to the system agent, [section 4](#4-tools-and-grants)). +**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)). | 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; a side panel on the desktop, 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 Rinx (native) and every script app with an agent, after first-use consent; see below | +| **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 Rinx (native) 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 | Each app peer has, separately from every other: - 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`; News declares `news.list`, `news.read` and `news.notify` (`apps/news/bundle/tools.json`, on its host service). 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 app's UI or its cards, 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. +- 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. Who gets a peer today (`crates/ai-host/src/lib.rs`, `Policy::shipped()`; `crates/app-peers/src/hosted.rs`, `effective_services` = declared ∩ supported ∩ policy): - **Rinx**, the only native app granted the assistant (the four `octos.*` services, from its `native-apps.json` `agent.octos`, generated into `crates/ai-host/src/native_agents.rs`), after first-use consent. -- **Script apps with an agent**: a manifest that declares `octos.*` or an `agent` block, or a bundle that ships `tools.json` (News). 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. +- **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. None does yet: the Terminal, the only process app, grants none. @@ -231,14 +238,14 @@ The kernel speaks the **octos UI protocol** (OUP; `octos-ui/v1alpha1`, JSON-RPC octos's peer mechanism, depth 1 (peers cannot create, steer or close peers). **On main** in octos; the tools are in octos `crates/octos-agent/src/tools/`. - **System agent → app agent.** `peer_send_input` (originator only, at most 64 KB) delivers text as the peer's next user turn, through the peer's inbox (a durable queue in serve, drained every few seconds, at least once). -- **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). +- **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 (the shell's overlay), 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. +- **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. ### The host-owned path: `peer/input` -For a host-owned peer, a plain `peer_send_input` runs as a kernel continuation without the app's tools or memory. ADR 0004 §6 fixes that: octos delivers the system agent's input to the host's driving connection as **`peer/input {peer, session_id, input_id, turn_id, text}`**, and the shell starts the turn itself (`turn/start` with the kernel's `turn_id`), so it runs with the app's tools, memory and context and its approvals surface in the app. If no host connection holds the peer, octos tells the system agent the app is not connected. **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)). 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 app-conversation sheet and question card (`broker::interrupt_where`). 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. +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. ### An app and its own agent @@ -246,7 +253,7 @@ For a host-owned peer, a plain `peer_send_input` runs as a kernel continuation w | --- | --- | --- | | 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)); no module uses it yet | | 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. An app that draws no chat (News) is talked to in the shell's "Ask " panel, on the same peer | On main ([#106](https://github.com/OctoSense-org/OctoSense/pull/106), consent from [#120](https://github.com/OctoSense-org/OctoSense/pull/120)) | +| 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`); no process app is granted an agent yet | ### Makepad's AI services bus vs OctoSense's app agents @@ -270,18 +277,20 @@ Where an agent's tools come from: | Source | Example | Runs | Status | | --- | --- | --- | --- | -| The app's own tools (`tools.json`: name `.`, schemas, `risk`, `confirm: host` or `app`, `shareable`) | `news.list`, `terminal.read_screen`, `mail.notify`, `calendar.add_event` | the app's host service, module or process, 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`) or the Terminal's AI bus service. News offers `news.list`, `news.read` and `news.notify`; the Terminal `terminal.run` and its read tools | -| The system toolbox, by capability (`research`, `crawl`, `model`) | `toolbox.search`, `toolbox.web_read`, `toolbox.deep_crawl`, `workflow.run` | host | In progress ([#108](https://github.com/OctoSense-org/OctoSense/pull/108)); `model` not registered yet | +| 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 `mail.notify`; 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.send` | 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. None of today's apps is granted one | -| 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, each call approved live through the router as a command, typed into the running Terminal (a process app on macOS, Windows, and Linux with Vulkan and Wayland; the in-process Terminal offers its reads only, and by ADR 0004 §10, decided 2026-09-29, stays read-only, so the grant takes effect only where a process Terminal exists). Granting it to app agents is step 11 | +| 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 | + +**What an agent puts on the glance screen** ([#267](https://github.com/OctoSense-org/OctoSense/pull/267), [#273](https://github.com/OctoSense-org/OctoSense/pull/273), [#274](https://github.com/OctoSense-org/OctoSense/pull/274)). **On main.** Every `.notify` fills the shell's one notice card (`crates/shell/resources/glance/notice.card`, `crates/shell/src/glance_notice.rs`): the app's icon and name come from the shell, the title and text from the call, and the same `card_id` replaces the app's earlier notice. Mail's and News's host services hand `notify` to it; the shell's notice service answers the apps without a service of their own. `calendar.notify` and `calendar.agenda` fill Calendar's own `event.card` and `agenda.card` (`apps/calendar/host-service/resources/`). The model writes text, never card code. Each card is published through the glance service (`crates/shell/src/glance.rs`) as the calling app, only when its manifest was granted `glance` (`glance::publish_for`), and posts a notification. On a desktop that is a toast, which opens the card in its own card window (`glance_sheet.rs`); every new card also opens the glance panel (`glance_panel.rs`, the bar's bell, F9) unless a card window is up, and the toasts stack left of the open panel (`crates/shell/src/shell/notifications.rs`, `keep_clear_of`). The hovered card shows a dismiss button (`glance::dismiss`), and a dismissal can be undone ([#290](https://github.com/OctoSense-org/OctoSense/pull/290)). On a phone it is a shade notification that opens the glance page. None of these cards declares an in-card chat (`sys.chat`); the only shipped card with one is the `OCTOSENSE_GLANCE_DEMO=mail` demo card (`crates/shell/resources/glance/mail-request.card`), whose chat answers with a canned reply. **The system agent's tool set** (`crates/kernel/src/system_tools.rs`, [#117](https://github.com/OctoSense-org/OctoSense/pull/117)). **On main**, enforced: -- 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). +- 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 a process (ADR 0004 §10, decided 2026-09-29): on Linux without Vulkan and on phones `terminal.run` has no target. +- **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. **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. @@ -289,22 +298,26 @@ Where an agent's tools come from: **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). -**Agent tool calls, not the person's own actions.** The router and the shell's sheets govern agent tool calls (`peer/tool/call`). What the person does in an app's own UI, its cards on the glance screen and behind notifications included, is the app's own action, run under the app's policy through the Card runner's service gate and each host service's own checks, with no extra shell approval (ADR 0004 §4 and §8, decided 2026-09-29; hardening of card actions deferred). An app can draw a look-alike approval card, but only the shell's host connection answers `approval/respond`. **In progress**: on main a glance tile is still contained (an empty policy, no input, one shell tap target; `crates/shell/src/glance_card.rs`); interactive cards are the PR branch `feat/interactive-glance-cards`. +**Agent tool calls, not the person's own actions.** The router and the shell's sheets govern agent tool calls (`peer/tool/call`). What the person does in an app's own UI, its cards on the glance screen and behind notifications included, is the app's own action, run under the app's policy through the Card runner's service gate and each host service's own checks, with no extra shell approval (ADR 0004 §4 and §8, decided 2026-09-29; hardening of card actions deferred). An app can draw a look-alike approval card, but only the shell's host connection answers `approval/respond`. **On main** since [#153](https://github.com/OctoSense-org/OctoSense/pull/153): every glance tile runs in its own isolate under the publishing app's resolved policy and takes input (`crates/shell/src/glance_card.rs`), and its `host.request` calls leave through the Card runner's gate as that app. A script card's handlers run wherever it is drawn. An L0 card's taps and field edits run in the desktop's card window (`glance_sheet.rs`, opened from the card's toast) and in the glance panel, one path for both (`glance_card.rs` `LiveCards`, [#278](https://github.com/OctoSense-org/OctoSense/pull/278)); on the phone's glance page they stay inert. **The approval router** (`crates/shell/src/approvals/router.rs`, [#120](https://github.com/OctoSense-org/OctoSense/pull/120)) is the shell's one place that answers approval requests, on their exact arguments. **On main.** For each request, in order: ```mermaid flowchart TB req["Approval request
(owning app, tool, exact args, caller, context)"] + ext{"0. An external
client's turn?"} + left(["left to that client:
nothing held or answered"]) dev{"1. Developer mode
covers the app?"} capp{"2. confirm: app?"} - always{"3. auto_approvable: false,
outcome unknown,
or an external client?"} + always{"3. auto_approvable: false,
outcome unknown,
or not on the host's connection?"} rule{"4. A standing rule on
(owning app, tool) matches?"} sheet["5. Shell-drawn sheet
(one per request, or batched
in the system chat)"] ok(["approved, audited"]) appsheet["The owning app's own sheet,
showing the caller
(waits, then refused visibly)"] person["The person"] - req --> dev + req --> ext + ext -- yes --> left + ext -- no --> dev dev -- yes --> ok dev -- no --> capp capp -- yes --> appsheet --> person @@ -316,9 +329,10 @@ 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`); Rinx at its pinned tag does not yet, so its send sheet still answers inside Rinx. -3. **`auto_approvable: false`**, **outcome unknown** and **external clients'** calls always go to the person. +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. @@ -326,7 +340,7 @@ Every decision reaches the relay once and the **audit** (`audit.rs`: one JSON li **First-use consent** (`consent.rs`): the first time an app asks for its agent, the shell shows what the agent may read and use and where the model runs; Settings lists every app's agent with an off switch (ADR 0004 §4). **On main** for native modules (`consent_for_module`, before the offer in `module_host.rs`) and script apps (`consent_for_contained`). -**What feeds the router.** 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. octos approvals that are not host tools and are raised in an app's context still go to that app: script apps' are declined in `contained.rs`, Rinx's are answered by the person in Rinx, and in developer mode `crates/app-peers/src/host_approvals.rs` answers them for the covered apps (audited). +**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`). ## 6. Storage and secrets @@ -346,13 +360,13 @@ One host-owned layout for every app, declared by its manifest (`storage` block) - **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 accepts only `storage.max_bytes` today, so script apps act for the device. +- **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, for hagency-org/Rinx#37: 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. +- **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 ([hagency-org/Rinx#37](https://github.com/hagency-org/Rinx/issues/37)); under an explicit `OCTOSENSE_HOME` the shell points it at `/apps/rinx/data` (`RINX_DATA_DIR`, set in `crates/shell/src/octosense/paths.rs`; an explicit `RINX_DATA_DIR` wins). +- **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). ## 7. Trust boundaries and isolation @@ -372,7 +386,7 @@ One host-owned layout for every app, declared by its manifest (`storage` block) +-------v----------+ +--------v--------------------+ | process apps | | octos kernel | | (OS sandbox: | | workspace fence per peer | - | macOS on main) | | external clients: allowlist| + | macOS, Linux) | | external clients: allowlist| +------------------+ +-----------------------------+ ``` @@ -380,7 +394,7 @@ One host-owned layout for every app, declared by its manifest (`storage` block) | --- | --- | --- | | 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 built and compile-checked but not yet run on a real kernel ([#138](https://github.com/OctoSense-org/OctoSense/issues/138)); Windows planned ([#137](https://github.com/OctoSense-org/OctoSense/issues/137)): a process app there runs with the user's rights | +| 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 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 | @@ -392,7 +406,7 @@ One host-owned layout for every app, declared by its manifest (`storage` block) ## 8. Worked example: emailing a meeting invite -This ADR 0004 example combines an implemented Calendar action with a **planned Mail send tool**. Calendar ships `calendar.add_event`; Mail currently exposes only `mail.notify` to its agent. Completing this workflow requires a declared/shareable `mail.send`, admission and caller grants, and its executor. The broker, relay and approval mechanisms below already exist. +This ADR 0004 example combines an implemented Calendar action with a **planned Mail send tool**. Calendar (desktop only) ships `calendar.add_event`; Mail currently exposes only `mail.notify` to its agent. Completing this workflow requires a declared/shareable `mail.send`, admission and caller grants, and its executor. The broker, relay and approval mechanisms below already exist. ```mermaid sequenceDiagram @@ -428,18 +442,18 @@ sequenceDiagram | 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`. | `peer_send_input` and relay grants implemented; this example's Mail tool/grant still planned | +| 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 only host-tool grant today is `terminal.run`, and this example's Mail tool and grant are still planned | | 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) 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 and relay implemented; `mail.send` planned | +| 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 (desktop only; 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 | -| 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 agent has `mail.notify`, while `mail.send` remains planned | +| 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 `mail.send` method serves Mail's own UI); 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 | +| 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 | ## Where the code and the ADRs disagree -Found while writing this page (2026-09-28) and rechecked against `main` on 2026-09-29 (the ADR 0004 gap review); items marked fixed were fixed in code or in the ADR text since. +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`). @@ -447,15 +461,14 @@ Found while writing this page (2026-09-28) and rechecked against `main` on 2026- 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** checks only `wasm32` while the function also excludes native mobile. -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`), so such a call would wait and be refused. The audit keeps a digest of the arguments, not the arguments. The send queue and undo window are not built. +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.** Only the macOS sandbox is built and tested; Linux is built but unvalidated (#138) and Windows is not built (#137). +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. - ## Source map | What | Where | @@ -472,4 +485,11 @@ Found while writing this page (2026-09-28) and rechecked against `main` on 2026- | 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) | +| 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) | diff --git a/docs/architecture.zh-CN.md b/docs/architecture.zh-CN.md index 870d3557..336f8a4d 100644 --- a/docs/architecture.zh-CN.md +++ b/docs/architecture.zh-CN.md @@ -10,7 +10,7 @@ OctoSense 的进程、Agent、工具、审批、存储与信任边界。具体 - **进行中**:有未合并的 pull request,附链接。 - **规划中**:已在 ADR 中决定(附链接和计划步骤),尚未实现。 -背后的决策是 [ADR 0001](adr/0001-one-octosense-repository.md)(英文,一个仓库)、[ADR 0002](adr/0002-event-driven-app-agents.md)(英文,应用 Agent,Proposed,经 0004 修订)、[ADR 0003](adr/0003-shared-octos-client-access.md)(英文,Talk to Octos)和 [ADR 0004](adr/0004-native-apps-hosting-and-peers.md)(英文,原生应用、应用 Agent、跨应用协作与审批,已接受)。助手的各项服务、各类应用目前能调用什么、如何在本地运行,见 [ai-services.zh-CN.md](ai-services.zh-CN.md);本文不再重复。 +背后的决策是 [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);本文不再重复。 ## 目录 @@ -32,17 +32,19 @@ OctoSense 的进程、Agent、工具、审批、存储与信任边界。具体 ![OctoSense AI 服务:一次已授权的跨应用调用](images/ai-services-cross-app-call.png) -*示意图(生成)。实线表示已在 main 上;虚线表示计划中或进行中(进程应用的 peer link;`peer/input` 和宿主工具自锁定 octos 665209e5 起已在 main,octos#2567)。图与下文不一致时以文字为准:例如 `peer_send_input` 把消息放入 peer 的收件箱,peer 在下一轮处理它。* +*示意图(生成)。实线表示已在 main 上;虚线表示计划中或进行中(进程应用的 peer link 自 [#130](https://github.com/OctoSense-org/OctoSense/pull/130) 起 Shell 一侧已在 main 上,但还没有进程应用使用它;`peer/input` 和宿主工具自锁定 octos 665209e5 起已在 main,octos#2567)。图与下文不一致时以文字为准:例如 `peer_send_input` 把消息放入 peer 的收件箱,peer 在下一轮处理它。* ```mermaid flowchart LR person(["用户"]) subgraph shellp["OctoSense Shell 进程(桌面端,或手机上的 Home)"] - wm["窗口管理器、启动器、面板
crates/shell"] + wm["窗口管理器、启动器、面板、
系统对话、glance 面板
crates/shell"] mods["进程内原生模块
App Hub、Rinx、(AppCard)"] runner["App Hub Card runner
脚本应用各在隔离环境中"] router["审批路由
crates/shell/src/approvals"] aihost["ai-host + app-peers broker
到内核的宿主连接"] + relay["宿主工具中继
crates/shell/src/host_tools"] + svc["应用宿主服务
mail、calendar、news;通知"] bus["AI 服务总线
crates/shell/src/ai_bus.rs"] end term["进程应用(桌面端)
Terminal"] @@ -60,6 +62,10 @@ flowchart LR aihost <-->|"OUP over stdio
(或宿主 token 的 WebSocket)"| kern sys -- "peer_send_input / 黑板" --> peers ext -. "外部 token、允许列表" .-> sys + kern -- "peer/tool/call、审批" --> relay + relay --> router + relay -- "应用工具" --> svc + svc -- "glance.publish:卡片" --> wm term -- "类型化工具" --> bus mods -- "类型化工具" --> bus bus --> router @@ -69,6 +75,7 @@ flowchart LR +--------------------------- OctoSense Shell 进程 -------------------------------+ | 窗口管理器 / 启动器 / 面板 审批路由 AI 服务总线 | | 进程内模块:App Hub(+ 运行脚本应用的 Card runner)、Rinx、(AppCard) | +| 宿主工具中继 -> 应用宿主服务(mail、calendar、news、通知) | | ai-host + app-peers broker == 宿主连接 ==+ | +--------------------------------------------|-----------------------------------+ ^ hub:回环 WebSocket | OUP:stdio(默认)或 @@ -119,22 +126,22 @@ flowchart LR | Sheets、Reference | 进程内 | 进程内 | 进程内 | 可选 / `mobile-apps` | – | | AppCard | 进程内 | 进程内 | 进程内 | 可选 / 可选 | 自己的内核连接 | -因此**只有 Terminal 是进程应用**,而且只在桌面端;`tools/native_apps.py` 拒绝 Linux 上的普通 `process`(只允许 `process-if-vulkan`),所以没有 Vulkan 和 Wayland 的 Linux 上所有应用都在进程内运行。App Hub 留在进程内,因为它承载所有脚本应用运行所在的 Card runner;Rinx 留在进程内,直到 peer link 和进程沙箱就绪(ADR 0004 §2)。 +因此**只有 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`。 -Shell 在运行时如何决定(`crates/shell/src/apps.rs`,`AppRegistry::hosting`):如果目标平台不能运行进程(在 wasm 和原生移动平台上 `host::processes_available()` 为 false),所有应用都作为模块;App Hub 和 Settings 始终是模块;其他情况下原生应用按清单条目托管,并且只有存在进程形态(可以 `cargo run` 的源码检出,或同目录下的二进制)时才作为进程运行。发布包目前还不附带进程应用的二进制([#94](https://github.com/OctoSense-org/OctoSense/pull/94),进行中),因此在发布包中会回退到进程内。按应用的覆盖设置(`~/.makepad/wm/apps.splash`、`--module `)可以把模块切换为进程。 +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 `)可以在模块和进程之间切换一个应用。 **进程托管**使用 Makepad 窗口管理器的托管机制(`crates/shell/src/clients.rs`、`hub.rs`): -- Shell 以 `--stdin-loop` 启动应用子进程(在源码检出中为 `cargo run … -- --stdin-loop`,否则为同目录下的二进制),放在独立的进程组中,并设置 `STUDIO_HOST=http://127.0.0.1:` 和其客户端 id。 -- 子进程回连 Shell 的 **hub**:Shell 在回环地址 8765–8785 中第一个空闲端口上绑定的 HTTP/WebSocket 服务(`WmHub::start`),使用 Makepad 的 studio 协议(`AppToStudio` / `StudioToApp`)。hub 只接受出示了 Shell 写入该子进程 stdin 的本次启动密钥的连接,每次启动只接受一次,并拒绝浏览器来源([ADR 0004 §5](adr/0004-native-apps-hosting-and-peers.md#5-the-peer-link-for-process-hosted-native-apps))。 +- 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 回读。 -- 进程应用崩溃只影响它自己:Shell 移除其客户端并显示 “App stopped”。(目前只有进程内模块有 Restart 界面;见[不一致之处](#代码与-adr-不一致之处)。) +- 进程应用崩溃只影响它自己:用户打开的窗口意外退出时,其磁贴保留,显示为已关闭并提供 Restart,同时发出 “App stopped”([#130](https://github.com/OctoSense-org/OctoSense/pull/130);`clients.rs` 中的 `ClientSlot::stops_in_place`)。预热实例、AI 面板、预览或正在关闭的客户端照旧消失。 **进程内托管**(`crates/shell/src/module_host.rs`):模块的每个实例都有自己的 splash 隔离环境(`alloc_splash_vm_with_network`)和存储命名空间。隔离环境只隔离脚本堆;模块的 Rust 代码与 Shell 共享内存。每次调用模块都在 `catch_unwind` 下进行(`contain`、`contain_outside`):panic 会把该模块标记为失败,把它进行中的工具调用答复为“结果未知”,关闭它的额外窗口并显示 Restart 界面,而 Shell 继续运行([#114](https://github.com/OctoSense-org/OctoSense/pull/114),ADR 0004 步骤 9)。**已在 main。** ### 脚本应用 -脚本应用(系统应用 News、Photos、Maps、仅手机上的 Camera、Mail、AI providers 和 YouTube,来自 `desktop/system-apps.json` 和 `phone/system-apps.json`,以及商店应用)是 OctoScript 应用包。它们都在进程内、在 App Hub 的 **Card runner**(`octosense-app-hub-app` 的 `CARD_MODULE`)中运行:每个应用实例一个嵌套隔离环境,去掉了 `mod.res` 和 `mod.run`,有 jail 和配额。应用只能通过 `host.request(".", …)` 调用其清单获授权的服务族来访问 Shell。脚本的错误只会在它自己的隔离环境中失败。**已在 main。** +脚本应用(系统应用 News、Photos、Maps、Mail、AI providers 和 YouTube,加上仅桌面端的 Calendar 和仅手机上的 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。** ```mermaid flowchart TB @@ -160,27 +167,27 @@ flowchart TB ## 2. Agent -**每个 Shell 一个内核;Agent 是会话,不是进程。** 在这一个内核中,Agent 回合由 Tokio 任务执行,peer 与 session 是持久状态。一个 peer 可包含多个 context session,一个回合也可创建多个任务。实际线程与任务对应关系见[导读](architecture-walkthrough.zh-CN.md#10-映射到-rust-的实际执行模型)。文件编辑之类的工具在内核中运行;octos 的命令类工具在会话的工作区中以短时子进程运行(OctoSense 不让系统 Agent 使用它们,见[第 4 节](#4-工具与授权))。 +**每个 Shell 一个内核;Agent 是会话,不是进程。** 在这一个内核中,Agent 回合由 Tokio 任务执行,peer 与 session 是持久状态。一个 peer 可包含多个 context session,一个回合也可创建多个任务,不存在一个 Agent 对应一个任务的关系。实际线程与任务对应关系见[导读](architecture-walkthrough.zh-CN.md#10-映射到-rust-的实际执行模型)。文件编辑之类的工具在内核中运行;octos 的命令类工具会在会话的工作区中以短时子进程运行,但 OctoSense 不把它们提供给任何 Agent(见[第 4 节](#4-工具与授权))。 | 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):设置 → 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;桌面上位于系统聊天旁,手机上为全屏面板)与它对话,面板显示两条通道及发言者、输入框和停止按钮 | Rinx(原生)和每个带 Agent 的脚本应用已在 main,需首次使用同意;见下文 | +| **系统 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” | Rinx(原生)和每个带 Agent 的脚本应用(目前是系统应用 News、Mail、Photos、Maps、YouTube、仅桌面端的 Calendar 和仅手机上的 Camera)已在 main,需首次使用同意;见下文 | 每个应用 peer 都独立拥有: - **工作区**: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` 设置模型); -- 自己的**工具列表**:内核对 peer 安全的默认工具,加上 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`。`main` 上还没有应用声明 `tools.json`,因此每个 peer 注册一个空集合,这仍然让它的回合获得应用的记忆和上下文(注册失败的 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 的对话(来自应用界面或其卡片)在用户通道中运行,这是一个以 `share_history` 打开的上下文,与 peer 自己的会话(系统 Agent 的通道)并行,并让每条通道看到另一条通道最近的回合(ADR 0004 §6,2026-09-29 决定;见下文)。在清单声明 Agent 工作在账号文件夹中时(`storage.agent_workspace: "account"`,octos#2647),该对话上下文以 octos 的 `read_parent` 打开:它只读地读取账号文件夹,从不读取其他上下文的文件夹,且只写入自己的文件夹。 +- 自己的**工具列表**:内核工具恰好是其清单授予的那些(`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` 打开:它只读地读取账号文件夹,从不读取其他上下文的文件夹,且只写入自己的文件夹。 目前谁有 peer(`crates/ai-host/src/lib.rs`,`Policy::shipped()`;`crates/app-peers/src/hosted.rs`,`effective_services` = 声明 ∩ 支持 ∩ 策略): -- **Rinx**,唯一获授权使用助手的原生应用(四个 `octos.*` 服务),需首次使用时的同意。 -- **带 Agent 的脚本应用**:清单声明了 `octos.*` 或 `agent` 块,或应用包带有 `tools.json`(如 News)。每个应用/账号一个 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`),因此即使应用从不调用 `octos`,系统 Agent 的 `peer_list` 也能看到它;对尚未同意或已关闭的应用,Shell 通过系统聊天中的提示以及 `agents.list` / `agents.ask` 宿主工具告知系统 Agent。 +- **Rinx**,唯一获授权使用助手的原生应用(四个 `octos.*` 服务,来自它在 `native-apps.json` 中的 `agent.octos`,生成到 `crates/ai-host/src/native_agents.rs`),需首次使用时的同意。 +- **带 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。 -- 进程应用目前没有 Agent:只有 `ModuleHost::create` 中才会提供服务(peer link 规划中,步骤 8)。 +- 其 `native-apps.json` 条目授予了 `agent.octos` 服务的**进程应用**,经 **peer link**(`crates/shell/src/peer_link/`,[#130](https://github.com/OctoSense-org/OctoSense/pull/130))访问自己的 peer,需首次使用同意。目前还没有这样的应用:唯一的进程应用 Terminal 没有授予任何服务。 peer 会被保留而不是丢弃:broker 用其宿主 token(存放在 `/../app-peers`,0600)恢复 peer,登出时也从不调用 `peer_close`,因为 octos 无法恢复已关闭的 peer,也无法为同一个(应用,账号)创建替代者(ADR 0004 §11)。 @@ -204,7 +211,7 @@ flowchart LR end rinx["Rinx(模块)"] -- "OctosAppService" --> broker card["脚本应用"] -- "host.request(octos.*)" --> broker - proc["进程应用"] -. "peer link(规划中)" .-> broker + proc["进程应用"] -. "peer link(Shell 一侧已在 main,尚无使用者)" .-> broker broker -- "OUP:peer/prepare、peer/context/open、turn/start" --> kernel sys -- "peer_send_input" --> p1 p1 -- "写入" --> bb @@ -231,13 +238,14 @@ flowchart LR 使用 octos 的 peer 机制,深度为 1(peer 不能创建、引导或关闭 peer)。octos 中**已在 main**;这些工具位于 octos `crates/octos-agent/src/tools/`。 - **系统 Agent → 应用 Agent。** `peer_send_input`(只有发起者可用,最多 64 KB)经 peer 的收件箱把文字作为 peer 的下一个用户回合送达(在 serve 中是持久队列,每隔几秒处理一次,至少送达一次)。 +- **系统 Agent 能联系到哪些应用的 Agent。** `peer_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 把每个问题转到正确的对话,没有 `host.ask`。**已在 main 上**:系统对话回答系统 Agent 自己会话上的问题;broker 把应用 peer 的 `user_question/requested`(其自身会话、请求上下文或 `peer/input` 回合)连同回合的发起者(octos 报告时用它,否则推断:broker 为 `peer/input` 发起的回合属于系统 Agent)交给 Shell 的请求模型(`crates/shell/src/questions/`),按发起者路由:用户或应用的回合转到应用的对话(Shell 的浮层),系统 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 预算。用户的“停止”结束两条通道上正在运行的回合。 +- **提问。** 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 预算。 ### 宿主拥有的路径:`peer/input` -对宿主拥有的 peer,普通的 `peer_send_input` 会作为内核续跑执行,没有应用的工具或记忆。ADR 0004 §6 解决了这个问题:octos 把系统 Agent 的输入以 **`peer/input {peer, session_id, input_id, turn_id, text}`** 的形式送到宿主的驱动连接,由 Shell 自己启动回合(用内核的 `turn_id` 调用 `turn/start`),这样回合就带有应用的工具、记忆和上下文,其审批也出现在应用中。如果没有宿主连接持有该 peer,octos 会告诉系统 Agent 该应用未连接。**已在 main**:自锁定 665209e5 起 octos 会发送它(octos [#2567](https://github.com/octos-org/octos/pull/2567));broker(`crates/app-peers/src/broker.rs`)用内核的 `turn_id` 在自己的连接上启动回合,丢弃重复的 `input_id`,peer 回合运行时把输入排队,对已登出的账号或用户未允许的应用不启动任何回合(`host_tools::ShellToolHost::admit_input`)。被拒绝的输入只记日志:octos 没有让宿主告诉系统 Agent 的途径,系统 Agent 听到的是 “queued”。Shell 的各个内核使用方共用一个内核连接(`crates/kernel/src/router.rs`),因此每个 broker 和系统对话对 octos 来说是同一个宿主连接;各自只回答自己的 peer 或会话的调用。 +对宿主拥有的 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 或会话的调用。 ### 应用与它自己的 Agent @@ -245,8 +253,8 @@ flowchart LR | --- | --- | --- | | 进程内原生模块,经 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));尚无模块使用 | | 进程内原生模块(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 审批路由,只有用户通过宿主界面回答 | 已在 main([#106](https://github.com/OctoSense-org/OctoSense/pull/106),同意机制来自 [#120](https://github.com/OctoSense-org/OctoSense/pull/120)) | -| 独立进程的原生应用 | **peer link**:应用 hub 连接上的独立通道(`PeerRequest`、`PeerReply`,以及由 Shell 盖上身份和调用方的 `PeerToolCall`),从不注册到 AI 总线;客户端 API 在 Makepad 的 `makepad-ai-services` 中 | 规划中(ADR 0004 §5,步骤 8);`hub.rs` 中尚无代码 | +| 脚本应用 | 向 `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 连接转发);尚无进程应用被授予 Agent | ### Makepad 的 AI 服务总线与 OctoSense 的应用 Agent @@ -269,18 +277,20 @@ Agent 的工具来源: | 来源 | 示例 | 在哪里运行 | 状态 | | --- | --- | --- | --- | -| 应用自己的工具(`tools.json`:名称 `.`、schema、`risk`、`confirm: host` 或 `app`、`shareable`) | `mail.send`、`rinx.message.send` | 应用的宿主服务、模块或进程,由 Shell 调用 | 已在 main 上注册和路由(`crates/shell/src/host_tools/`):按(所属应用,工具)和调用方授权,盖上账号、上下文和客户端,路由到进程应用的 peer link 或进程内模块的执行器(`OctosAppService::set_tool_executor`)。还没有应用声明 `tools.json`,所以尚未注册任何工具;Rinx 的工具经总线到达 AI 面板 | -| 系统工具箱,按能力授予(`research`、`crawl`、`model`) | `toolbox.search`、`toolbox.web_read`、`toolbox.deep_crawl`、`workflow.run` | 宿主 | 进行中([#108](https://github.com/OctoSense-org/OctoSense/pull/108));`model` 尚未注册 | +| 应用自己的工具(`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`;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.send` | 所属应用,经 Shell | 已在 main 上注册、逐次检查并路由(`relay::Catalog`);还没有任何授权(App Hub 安装时授权和 `native-apps.json` 授权属于步骤 6) | -| 命令执行 | `terminal.run`(Terminal 的可共享工具:`confirm: host`、`auto_approvable: false`) | 宿主工具,在用户可见的终端中 | 系统 Agent 已在 main:Setup › Assistant › Command execution 开启时注册在其会话上,每次调用经路由作为命令实时批准,输入到正在运行的 Terminal(macOS 和 Windows 上是进程应用;进程内 Terminal 只提供读取工具)。授予应用 Agent 属于步骤 11 | +| 其他应用可共享的工具,由 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 | + +**Agent 放到 glance 屏幕上的内容**([#267](https://github.com/OctoSense-org/OctoSense/pull/267)、[#273](https://github.com/OctoSense-org/OctoSense/pull/273)、[#274](https://github.com/OctoSense-org/OctoSense/pull/274))。**已在 main。** 每个 `.notify` 都填充 Shell 的同一张通知卡片(`crates/shell/resources/glance/notice.card`、`crates/shell/src/glance_notice.rs`):应用的图标和名称由 Shell 填入,标题和正文来自调用,同一个 `card_id` 会替换该应用之前的通知。Mail 和 News 的宿主服务把 `notify` 交给它;没有自己服务的应用由 Shell 的通知服务应答。`calendar.notify` 和 `calendar.agenda` 填充 Calendar 自己的 `event.card` 和 `agenda.card`(`apps/calendar/host-service/resources/`)。模型只写文字,从不编写卡片代码。每张卡片都经 glance 服务(`crates/shell/src/glance.rs`)以调用方应用的身份发布,前提是它的清单获准使用 `glance`(`glance::publish_for`),并发出一条通知。在桌面端,通知是一个 toast,点击它会在单独的卡片窗口中打开这张卡片(`glance_sheet.rs`);每来一张新卡片,glance 面板(`glance_panel.rs`,顶栏的铃铛、F9)也会打开,除非卡片窗口已经打开,而 toast 叠放在打开的面板左侧(`crates/shell/src/shell/notifications.rs`,`keep_clear_of`)。鼠标悬停的卡片会显示一个移除按钮(`glance::dismiss`),移除可以撤销([#290](https://github.com/OctoSense-org/OctoSense/pull/290))。在手机上,通知是通知栏中的一条通知,点击它会打开 glance 页面。这些卡片都没有声明卡内对话(`sys.chat`);唯一带对话的内置卡片是 `OCTOSENSE_GLANCE_DEMO=mail` 的演示卡片(`crates/shell/resources/glance/mail-request.card`),它的对话用固定的演示回复作答。 **系统 Agent 的工具集**(`crates/kernel/src/system_tools.rs`,[#117](https://github.com/OctoSense-org/OctoSense/pull/117))。**已在 main**,已生效: -- 默认列表 `SYSTEM_AGENT_TOOLS`:监督(`peer_send_input`、`peer_gather`、`peer_list`、`peer_respond`;不含 `peer_handoff`,也不含 `peer_close`,`_main` 配置文件的 `tool_policy` 对所有智能体禁用它)、其工作区的文件工具、`ask_user_question` 和查看媒体、记忆、`web_search` / `web_fetch`、`tool_search`。授权在此基础上增加(`SystemAgentTools`:工具箱工具、其他应用的可共享工具、命令执行)。 +- 默认列表 `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 启动之前就会立即提供该工具。 +- **从 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。 **外部客户端**只有 octos 的固定允许列表([第 3 节](#内核与客户端之间的-oup));宿主路由的工具、开发者授权和 `dev.run` 永远到不了它们。 @@ -288,22 +298,26 @@ Agent 的工具来源: **授权不等于审批。** 授权表示 Agent 可以*拥有*某个工具;审批表示*这一次*调用、带着这些确切参数,可以执行。只读和应用内操作类工具授权后即可运行;对外或破坏性的调用(发送、发布、分享、购买、删除、运行命令)需要用户实时批准或由常设规则批准。只有用户能批准;系统 Agent 从不批准,Agent 自己输出的文字也从不作为审批界面(ADR 0004 §8)。 -**Agent 的工具调用,而不是用户自己的操作。** 审批路由和 Shell 的面板管的是 Agent 的工具调用(`peer/tool/call`)。用户在应用自己的界面中所做的操作,包括它在速览屏和通知背后的卡片,都是应用自己的操作:在应用的策略下,经 Card runner 的服务关口和各宿主服务自己的检查运行,Shell 不再另加审批(ADR 0004 §4 和 §8,2026-09-29 决定;卡片操作的安全加固推迟)。应用可以画出貌似审批卡片的界面,但只有 Shell 的宿主连接能回答 `approval/respond`。**进行中**:main 上速览磁贴仍是受限的(空策略、不接受输入、整个磁贴是 Shell 的一个点按目标;`crates/shell/src/glance_card.rs`);可交互卡片在 PR 分支 `feat/interactive-glance-cards`。 +**Agent 的工具调用,而不是用户自己的操作。** 审批路由和 Shell 的面板管的是 Agent 的工具调用(`peer/tool/call`)。用户在应用自己的界面中所做的操作,包括它在 glance 屏幕上和通知背后的卡片,都是应用自己的操作:在应用的策略下,经 Card runner 的服务关口和各宿主服务自己的检查运行,Shell 不再另加审批(ADR 0004 §4 和 §8,2026-09-29 决定;卡片操作的安全加固推迟)。应用可以画出貌似审批卡片的界面,但只有 Shell 的宿主连接能回答 `approval/respond`。自 [#153](https://github.com/OctoSense-org/OctoSense/pull/153) 起**已在 main**:每个 glance 磁贴都在自己的隔离环境中、按发布应用解析后的策略运行,并接受输入(`crates/shell/src/glance_card.rs`),它的 `host.request` 调用以该应用的身份经过 Card runner 的关口。脚本卡片的处理函数在它被绘制的任何地方都会运行。L0 卡片的点按和字段编辑在桌面端的卡片窗口(`glance_sheet.rs`,从卡片的 toast 打开)和 glance 面板中运行,两者走同一条路径(`glance_card.rs` 的 `LiveCards`,[#278](https://github.com/OctoSense-org/OctoSense/pull/278));在手机的 glance 页面上,它们不起作用。 **审批路由**(`crates/shell/src/approvals/router.rs`,[#120](https://github.com/OctoSense-org/OctoSense/pull/120))是 Shell 中唯一回答审批请求的地方,依据的是确切参数。**已在 main。** 对每个请求依次: ```mermaid flowchart TB req["审批请求
(所属应用、工具、确切参数、调用方、上下文)"] + ext{"0. 外部客户端
的回合?"} + left(["留给该客户端:
不持有、不回答"]) dev{"1. 开发者模式
覆盖该应用?"} capp{"2. confirm: app?"} - always{"3. auto_approvable: false、
结果未知,
或外部客户端?"} + always{"3. auto_approvable: false、
结果未知,
或不在宿主自己的连接上?"} rule{"4. 有针对(所属应用,工具)
的常设规则匹配?"} sheet["5. Shell 绘制的面板
(每个请求一个,或在系统对话中
合并为一个)"] ok(["批准,已审计"]) appsheet["所属应用自己的面板,
显示调用方
(等待,超时后明确拒绝)"] person["用户"] - req --> dev + req --> ext + ext -- 是 --> left + ext -- 否 --> dev dev -- 是 --> ok dev -- 否 --> capp capp -- 是 --> appsheet --> person @@ -315,9 +329,10 @@ 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` 接入);所锁定标签的 Rinx 还没有这样做,因此它的发送面板仍在 Rinx 内部作答。 -3. **`auto_approvable: false`**、**结果未知**和**外部客户端**的调用总是交给用户。 +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` 审批可以合并到它对话中的一个面板。 @@ -325,7 +340,7 @@ flowchart TB **首次使用同意**(`consent.rs`):应用第一次请求它的 Agent 时,Shell 显示该 Agent 可以读取和使用什么,以及模型在哪里运行;Settings 列出每个应用的 Agent 并提供关闭开关(ADR 0004 §4)。原生模块(`module_host.rs` 中在提供服务之前调用 `consent_for_module`)和脚本应用(`consent_for_contained`)**已在 main**。 -**谁在向路由提交请求。** AI 服务总线:面板对 `native-apps.json` 中列出的 `confirm: host` 工具(目前是 Terminal 的 `run`)的调用会被挂起(`bus::`),直到路由作答。以及 octos,经宿主工具中继(`host_tools::init` 在启动时用 `set_relay` 安装它):每个 `host_tool` 审批(有门控的 `confirm: host` 应用工具,只发给宿主连接),附所属应用、确切参数和调用方;以及每个 `confirm: app` 调用,先确认收到,再交给所属应用的面板;系统对话也以同样方式把其会话的审批交给路由。不是宿主工具、在应用上下文中发起的 octos 审批仍交给该应用:脚本应用的在 `contained.rs` 中被拒绝,Rinx 的由用户在 Rinx 中回答,开发者模式下由 `crates/app-peers/src/host_approvals.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`)。 ## 6. 存储与机密 @@ -342,16 +357,16 @@ flowchart TB - **OctoSense 主目录**在手机上是平台的应用数据目录,否则为 `~/.octosense`(`OCTOSENSE_HOME` 可覆盖;`crates/shell/src/octosense/paths.rs`)。应用根目录可用 `OCTOSENSE_APP_DATA` 移动;机密根目录始终是 `/secrets`,两者从不重叠。目录权限为 0700,拒绝路径中的符号链接。 - **账号 hash** 是对规范化账号 id 做带域分隔的 SHA-256 后取 128 位(`account_hash`)。 -- **按 ADR,Agent 的工作区就是账号目录。** 目前目录已存在,模块可以通过 `AppStorage::agent_workspace` 获得它,但 broker 还没有把它作为 peer 的 `cwd` 传给 `peer/prepare`:peer 在内核分配的工作区中工作(规划中,步骤 10)。 +- **按 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 的清单目前只接受 `storage.max_bytes`,因此脚本应用按设备运行。 +- **清单到达宿主**(`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):在 `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 无需改动。 +- **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 仍使用自己的数据目录([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` 优先)。 +- **脚本应用**保留 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` 优先)。 ## 7. 信任边界与隔离 @@ -371,7 +386,7 @@ flowchart TB +-------v----------+ +--------v--------------------+ | 进程应用 | | octos 内核 | | (操作系统沙箱: | | 每个 peer 的工作区围栏 | - | 规划中) | | 外部客户端:允许列表 | + | macOS、Linux) | | 外部客户端:允许列表 | +------------------+ +-----------------------------+ ``` @@ -379,19 +394,19 @@ flowchart TB | --- | --- | --- | | 脚本应用 ↔ Shell | App Hub 的嵌套隔离环境:没有 `mod.res` / `mod.run`,有 jail 和配额,只能对获授权的服务族使用 `host.request`;密码输入框失效;机密在宿主面板上输入 | 已在 main | | 原生模块 ↔ Shell | 内存上没有边界:只接受经评审的第一方代码(`native-apps.json`);模块边界上的 **panic 隔离**(`catch_unwind`,[#114](https://github.com/OctoSense-org/OctoSense/pull/114));局限:展开过程中再次 panic、`panic = "abort"`、FFI | 已在 main | -| 进程应用 ↔ Shell | 独立的地址空间;依据清单的 `sandbox` 和 `storage` 生成的**操作系统沙箱**(macOS 沙箱配置、Linux Landlock 和 seccomp、Windows AppContainer) | 沙箱规划中(ADR 0004 §3,步骤 8);目前进程应用以用户权限运行 | -| 应用 ↔ 内核 | 应用从不使用内核协议,**永远看不到宿主 token**:模块得到受限的 `OctosAppService`,脚本应用得到宿主服务,进程应用(规划中)使用 peer link | 已在 main | +| 进程应用 ↔ 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**:模块得到受限的 `OctosAppService`,脚本应用得到宿主服务,进程应用使用 peer link | 已在 main | | peer ↔ peer | octos:独立的工作区(拒绝重叠的工作区)、记忆命名空间、对话记录;请求上下文被限定在 `contexts//` 内 | 已在 main(octos UPCR-2026-034) | | Agent ↔ 机密 | 机密位于所有 jail 和工作区之外;启动检查 | 已在 main | | 外部客户端 ↔ 内核 | 外部 token、方法和工具允许列表、`Host` 和 origin 检查、无法访问 peer | 已在 main([ADR 0003](adr/0003-shared-octos-client-access.md)) | -**Shell 在每次调用时检查什么**(ADR 0004 §3):授权和同意;预算、速率限制和后台策略;每次工具调用的名称和参数是否符合声明的 `tools.json`;每个结果是否符合其 schema 和大小;自己的审计;崩溃清理。目前 main 上已有:同意;应用工具调用的参数按声明的 `input_schema`(及 64 KiB)检查,结果按 `output_schema` 和 256 KiB 检查,每个调用方 Agent 有预算(每回合、每天的调用次数,来自 `native-apps.json` 的 `agent.budget`,默认 32 和 1000);按精确名称授予 `octos.*` 服务(声明 ∩ 支持 ∩ 策略);脚本应用的参数规则和大小上限(文字最多 32 KiB,回复最多 2 MiB);总线调用的审批路由和审计。对应用工具调用(`crates/shell/src/host_tools/`):按(所属应用,工具)和调用方的授权、同意、已登出的账号、确认、每次出现只执行一次、取消后不再执行。参数和结果的 schema 检查以及 octos 之外的预算尚未实现。用户在应用卡片中的操作不经过这个中继:它们走应用自己的路径(ADR 0004 §4),因此这些检查从不覆盖它们。 +**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),因此这些检查从不覆盖它们。 **谁都无法检查的**:原生应用的代码在它自己的工具里做了什么,或它为什么发起一个回合。控制手段是评审,以及进程应用的沙箱。 ## 8. 完整示例:用邮件发送会议邀请 -此 ADR 0004 示例组合了已实现的 Calendar 操作与**规划中的 Mail 发信工具**。Calendar 已提供 `calendar.add_event`;Mail 当前只向 Agent 暴露 `mail.notify`。完成流程还需要声明可共享的 `mail.send`、准入与调用方授权,以及执行器。下方的 broker、relay 与审批机制已经实现。 +此 ADR 0004 示例组合了已实现的 Calendar 操作与**规划中的 Mail 发信工具**。Calendar(仅桌面端)已提供 `calendar.add_event`;Mail 当前只向 Agent 暴露 `mail.notify`。完成流程还需要声明可共享的 `mail.send`、准入与调用方授权,以及执行器。下方的 broker、relay 与审批机制已经实现。 ```mermaid sequenceDiagram @@ -426,32 +441,33 @@ sequenceDiagram | 步骤 | 发生什么 | 状态 | | --- | --- | --- | -| 1 | 用户在系统对话 `_main:api:octosense#system` 中向系统 Agent 提出请求。 | 目前只能从已配对的 Talk to Octos 客户端访问;Shell 还没有自己的系统 Agent 对话界面(系统助手面板与应用的 Makepad `aichat` 总线接口分别实现) | -| 2 | 系统 Agent 做计划。有歧义就提问,不去猜(“两个 Edward?”);有限的读取可以直接调用获授权的工具。需要 Calendar 自己判断的工作用 `peer_send_input` 交给 Calendar 的 Agent。 | `peer_send_input` 与 relay 授权已实现;此示例的 Mail 工具/授权仍在规划中 | +| 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 目前唯一的宿主工具授权是 `terminal.run`,此示例的 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`(自己的工具),并为每位受邀者调用一次 Mail 可共享的 `mail.send`。每次调用都以 `peer/tool/call` 到达 Shell;Shell 盖上调用方(Calendar 的 Agent)、账号和上下文。 | Calendar 操作与中继已实现;`mail.send` 规划中 | +| 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` 审批提交请求 | -| 7 | 批准后,Shell 把每次调用交给 Mail 的宿主服务,它用用户在 Mail 宿主面板上登录的账号发送(密码永远不会到达 Agent),并把结果以 `peer/tool/result` 返回内核。 | Mail 宿主服务已实现;Agent 已有 `mail.notify`,`mail.send` 仍在规划中 | +| 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` 方法服务于 Mail 自己的界面);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 自己的界面会显示变化,因为它们的数据变了。 | 规划中 | +| 9 | 系统 Agent 宣布“已预订周二下午 3 点;已向 3 人发送邀请”。Calendar 和 Mail 自己的界面会显示变化,因为它们的数据变了。 | 规划中:Calendar 自己的窗口还不能列出日程(它需要 App Hub 的 `calendar` 能力);如今 `calendar.notify` 可以把日程放到 glance 屏幕上 | ## 代码与 ADR 不一致之处 -下列记录始于 2026-09-28;已修复项随后由代码或 ADR 变更解决。应用 Agent 的关键路径于 2026-10-01 重新核对,见最后一项。 +撰写本文时(2026-09-28)发现,2026-09-29 对照 `main` 复查(ADR 0004 差距审查),2026-10-02 再次复查;标为已修复的条目此后已在代码或 ADR 文本中修复。 1. **Terminal 之外的进程应用。** 已修复:可选的 Sheets 和 Reference 在所有目标上都是 `module`,生成器拒绝 Linux 上的普通 `process`(ADR 0004 §2)。 -2. **崩溃进程应用的重启。** ADR 0004 §2 说它的磁贴会显示已关闭并提供重启。Shell 实际上移除客户端并发出 “App stopped” 通知;Restart 界面只用于进程内模块(`module_view.rs`)。 -3. **Agent 工作区 = 账号目录。** 部分修复:自锁定 octos 665209e5 起新建的 peer 以账号目录作为 `peer/prepare` 的 `cwd`;之前创建的 peer 保留内核分配的工作区,因为 octos 只允许在创建时的工作区下恢复 peer(迁移需要 octos 尚未提供的支持)。账号目录名(`account_hash`,SHA-256)和记忆命名空间标签(`broker.rs` 中的 FNV-1a)仍是对账号的两种不同 hash;改动标签会让每个应用的记忆重新计算键。 -4. **ADR 0003 的 “What the profile runs”** 说 OctoSense 既不配置工具集也不配置沙箱。自 [#117](https://github.com/OctoSense-org/OctoSense/pull/117) 起,Shell 每次启动前都向 `_main` profile 写入拒绝 `group:runtime` 的 `tool_policy`,因此宿主自己的回合也没有 octos shell。 +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()` 的测试**只检查 `wasm32`,而函数本身还排除了原生移动平台。 -8. **审批,ADR 0004 §8。** 每次应用工具调用都通过 `peer/tool/call` 到达 Shell,每个有门控的调用都到达路由(见上文)。还没有应用注册自己的 `confirm: app` 面板(Rinx 需要通过 `OctosAppService::set_confirm_sheet` 交出它的发送面板),因此这类调用会等待后被拒绝。审计记录的是参数摘要而不是参数。发送队列和撤销窗口尚未实现。 +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. **2026-09-29 应用 Agent 审查的后续,2026-10-01 复核。** “所有审批被丢弃”“全部固定为 `device`”“输入启动失败只记日志”等旧描述已不符合当前实现。broker 将问题/审批交给宿主;`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)。 - +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)。 ## 源码位置 @@ -469,4 +485,11 @@ sequenceDiagram | 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/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) |