From 7941ff7412245d0e9a9f6df4e846b635ee90c3e1 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Mon, 28 Sep 2026 10:20:41 -0700 Subject: [PATCH 1/2] =?UTF-8?q?toolbox-peers:=20offer=20the=20system=20too?= =?UTF-8?q?lbox=20to=20app=20agents=20(ADR=200002=20=C2=A76,=20M3/M5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Behind features, off by default until octos#2567 is on the pin: - app-peers `peer-tools`: every broker registers its peer's tool set (possibly empty) with `peer/tools/register` right after each successful `peer/prepare` and after every reconnect, on the link that drives its turns; a failed registration leaves the peer unbound, so no turn starts (issue #62). `peer/tool/call` runs `BrokerConfig::host_tools` once per occurrence, refuses unregistered tools, honours `peer/tool/cancel`, the call's deadline, release and link loss, and answers `peer/tool/result`. - toolbox `peer` module: the tools.json entries per grant (`research`: workflow.run, workflow.fork, toolbox.search, toolbox.web_read; `crawl` with crawl limits: toolbox.deep_crawl; never deep_research), and `PeerToolbox::call`, which checks the grant again and runs with the app's AppContext; deep_crawl over `ResearchBackend::read_links`; `scope::narrow_crawl` (octos `crawl_args`, with a compat mirror). - ai-host `toolbox-peers`: ToolboxTools (a worker thread per app for the non-Send toolbox), the OctosResearch backend, a ModelClient over `ModelHost::complete` (same ledger and daily budget as model.complete), grants from the manifest (TEMPORARY: os.* only until App Hub verifies research/crawl), results in `/.host/toolbox/` (`toolbox_folder`, always compiled, for #87). - llm-service: host callers (system set) may send up to 256 KiB of input. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/apps.yml | 7 + Cargo.lock | 4 + .../host-service/src/complete/mod.rs | 13 +- .../host-service/tests/complete.rs | 22 + crates/ai-host/Cargo.toml | 21 + crates/ai-host/README.md | 50 +- crates/ai-host/src/lib.rs | 43 ++ crates/ai-host/src/toolbox_peers.rs | 347 +++++++++ crates/ai-host/tests/toolbox_peers.rs | 457 ++++++++++++ crates/ai-host/tests/toolbox_real_kernel.rs | 134 ++++ crates/app-peers/Cargo.toml | 8 + crates/app-peers/README.md | 42 ++ crates/app-peers/src/broker.rs | 165 ++++- crates/app-peers/src/hosted.rs | 28 +- crates/app-peers/src/lib.rs | 2 + crates/app-peers/src/peer_tools.rs | 471 ++++++++++++ .../tests/fixtures/mock_agent_llm.py | 15 +- crates/app-peers/tests/peer_tools.rs | 478 ++++++++++++ crates/app-peers/tests/real_kernel.rs | 116 +++ crates/toolbox/README.md | 4 +- crates/toolbox/src/lib.rs | 1 + crates/toolbox/src/peer.rs | 697 ++++++++++++++++++ crates/toolbox/src/research/mod.rs | 26 + crates/toolbox/src/research/octos.rs | 99 ++- crates/toolbox/src/scope.rs | 71 ++ crates/toolbox/src/scope/compat.rs | 29 +- crates/toolbox/tests/peer.rs | 168 +++++ 27 files changed, 3505 insertions(+), 13 deletions(-) create mode 100644 crates/ai-host/src/toolbox_peers.rs create mode 100644 crates/ai-host/tests/toolbox_peers.rs create mode 100644 crates/ai-host/tests/toolbox_real_kernel.rs create mode 100644 crates/app-peers/src/peer_tools.rs create mode 100644 crates/app-peers/tests/peer_tools.rs create mode 100644 crates/toolbox/src/peer.rs create mode 100644 crates/toolbox/tests/peer.rs diff --git a/.github/workflows/apps.yml b/.github/workflows/apps.yml index 6b7dfb08..b52017c2 100644 --- a/.github/workflows/apps.yml +++ b/.github/workflows/apps.yml @@ -70,6 +70,10 @@ jobs: cargo test --locked -p octosense-app-peers --features octos-core,ws cargo clippy --locked -p octosense-app-peers --all-targets --no-deps -- -D warnings cargo clippy --locked -p octosense-app-peers --all-targets --features octos-core,ws --no-deps -- -D warnings + # Host-registered peer tools (octos#2567): off by default, tested + # against the scripted kernel. + cargo test --locked -p octosense-app-peers --features octos-core,ws,peer-tools + cargo clippy --locked -p octosense-app-peers --all-targets --features octos-core,ws,peer-tools --no-deps -- -D warnings - name: AI providers config run: cargo test --locked -p octosense-llm-config - name: System toolbox (workflow templates) @@ -106,6 +110,9 @@ jobs: cargo test --locked -p octosense-ai-host cargo test --locked -p octosense-ai-host --features octos-core,llm cargo clippy --locked -p octosense-ai-host --all-targets --features octos-core,llm --no-deps -- -D warnings + # The system toolbox for app agents (octos#2567): off by default. + cargo test --locked -p octosense-ai-host --features toolbox-peers + cargo clippy --locked -p octosense-ai-host --all-targets --features toolbox-peers --no-deps -- -D warnings - name: AppCard runtime setup safeguards working-directory: apps/appcard run: PYTHONPATH=tools python3 -m unittest core.test_native_runtime diff --git a/Cargo.lock b/Cargo.lock index c74b3662..c7dd9fcd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7271,9 +7271,13 @@ dependencies = [ "makepad-app-module", "makepad-widgets", "octosense-app-peers", + "octosense-appstore", "octosense-kernel", "octosense-llm-config", "octosense-llm-service", + "octosense-toolbox", + "serde_json", + "tokio", ] [[package]] diff --git a/apps/ai-providers/host-service/src/complete/mod.rs b/apps/ai-providers/host-service/src/complete/mod.rs index b96449f7..8d0fe695 100644 --- a/apps/ai-providers/host-service/src/complete/mod.rs +++ b/apps/ai-providers/host-service/src/complete/mod.rs @@ -53,7 +53,9 @@ //! to call it too, so direct calls and template calls share one budget per //! app: its `ModelRequest {system, user, output_schema}` maps onto a //! [`Request`] with `system` set and `input` the user document ([`host`] -//! hands out the registered host). +//! hands out the registered host). A host caller's input may be up to +//! [`HOST_INPUT_MAX`] (a digest carries its articles' evidence); an app's +//! stays at [`INPUT_MAX`]. use crate::model::{effective_model, ProfileStore}; use crate::probe; use crate::vault::Vault; @@ -77,6 +79,10 @@ pub const FAMILY: &str = "model"; pub const TASK_MAX: usize = 4 * 1024; /// The input, as compact JSON, in bytes. pub const INPUT_MAX: usize = 32 * 1024; +/// The input of a host caller's call (`system` set: the toolbox's digests, +/// whose user document carries the articles' evidence), which +/// `host.request` can never make. +pub const HOST_INPUT_MAX: usize = 256 * 1024; /// The model's reply text (after a code fence is removed), in bytes: the /// host's hard cap on what an app gets back. pub const OUTPUT_MAX: usize = 16 * 1024; @@ -219,8 +225,9 @@ impl Request { return Err(bad(format!("task is over {TASK_MAX} bytes"))); } let input = self.input.to_string().len(); - if input > INPUT_MAX { - return Err(bad(format!("input is {input} bytes; the most is {INPUT_MAX}"))); + let max = if self.system.is_some() { HOST_INPUT_MAX } else { INPUT_MAX }; + if input > max { + return Err(bad(format!("input is {input} bytes; the most is {max}"))); } if self.schema.is_null() { return Err(bad("schema is required".into())); diff --git a/apps/ai-providers/host-service/tests/complete.rs b/apps/ai-providers/host-service/tests/complete.rs index 975af1d3..779ff5bd 100644 --- a/apps/ai-providers/host-service/tests/complete.rs +++ b/apps/ai-providers/host-service/tests/complete.rs @@ -466,3 +466,25 @@ fn a_host_caller_shares_the_same_ledger() { assert_eq!(body["messages"][1]["content"], "{\"query\":\"x\"}"); assert_eq!(Code::Budget.as_str(), "budget"); } + +#[test] +fn a_host_caller_may_send_a_larger_input_than_an_app() { + // A toolbox digest carries the articles' evidence (up to 6 KB each): + // over an app's 32 KiB, within the host's 256 KiB. + let rig = Rig::new("host-input", vec![deepseek("deepseek-v4-flash")]); + rig.fake.says(GOOD); + let big = "x".repeat(complete::INPUT_MAX + 1024); + let request = |system: Option<&str>| Request { + class: complete::Class::Fast, + task: if system.is_some() { String::new() } else { "t".into() }, + input: json!(big), + schema: args()["schema"].clone(), + allow_urls: true, + system: system.map(str::to_owned), + }; + let refused = rig.host.complete(APP, request(None)).unwrap_err(); + assert_eq!(refused.code, Code::BadRequest); + rig.host.complete(APP, request(Some("You write a digest."))).unwrap(); + let huge = Request { input: json!("x".repeat(complete::HOST_INPUT_MAX + 1)), ..request(Some("s")) }; + assert_eq!(rig.host.complete(APP, huge).unwrap_err().code, Code::BadRequest); +} diff --git a/crates/ai-host/Cargo.toml b/crates/ai-host/Cargo.toml index 5bd8ee3e..e334dcf1 100644 --- a/crates/ai-host/Cargo.toml +++ b/crates/ai-host/Cargo.toml @@ -21,6 +21,8 @@ # dependencies below): `cfg(kernel)` in the source, set by build.rs. # - `llm`: register the `llm` host service (a shell's `app-hub` feature turns # it on; phones always). +# - `toolbox-peers`: the system toolbox as app peers' host-registered tools +# (octos#2567); off by default, see below. # # The kernel is crates/kernel, the app-peers contract and broker # crates/app-peers, the `llm` service and its config apps/ai-providers/*; @@ -38,6 +40,21 @@ license.workspace = true default = [] octos-core = ["dep:octosense-kernel", "octosense-app-peers/octos-core", "octosense-llm-service?/octos-core"] llm = ["dep:octosense-llm-service"] +# The system toolbox offered to app agents (ADR 0002 section 6, News M5): +# every app peer registers its toolbox tools (possibly none) with the kernel +# and the host runs their calls. Off by default: it needs a kernel with +# octos#2567 (host-registered peer tools); a kernel without it refuses the +# registration and the broker then starts no turns, Rinx's included. +toolbox-peers = [ + "octos-core", + "llm", + "octosense-app-peers/peer-tools", + "dep:octosense-toolbox", + "octosense-toolbox/octos-engine", + "dep:octosense-appstore", + "dep:serde_json", + "dep:tokio", +] [dependencies] makepad-widgets = { workspace = true } @@ -48,6 +65,10 @@ octosense-llm-config = { workspace = true } octosense-app-peers = { workspace = true } octosense-kernel = { workspace = true, optional = true } octosense-llm-service = { workspace = true, optional = true } +octosense-toolbox = { workspace = true, optional = true } +octosense-appstore = { workspace = true, optional = true } +serde_json = { workspace = true, optional = true } +tokio = { workspace = true, optional = true, features = ["rt", "sync", "macros"] } # Native mobile builds always carry the kernel (Android bundles liboctos.so, # OpenHarmony links the core in-process, iOS runs none) and, when the `llm` diff --git a/crates/ai-host/README.md b/crates/ai-host/README.md index c7e018bb..f80759f3 100644 --- a/crates/ai-host/README.md +++ b/crates/ai-host/README.md @@ -36,11 +36,55 @@ QrImport` (`platform()` or `paste_only()`); `policy: Policy` (`Policy::shipped()` grants Rinx the `octos.*` services). Features: `octos-core` (the kernel, app-peers broker, llm restart; native -mobile targets always have it — `cfg(kernel)`, set by build.rs) and `llm` -(register the `llm` service; a shell's `app-hub` turns it on). +mobile targets always have it — `cfg(kernel)`, set by build.rs), `llm` +(register the `llm` service; a shell's `app-hub` turns it on) and +`toolbox-peers` (below; off by default). + +## The system toolbox for app agents (`toolbox-peers`) + +ADR 0002 section 6 and News M5: the toolbox's tools are registered with each +app's peer (octos#2567, host-registered peer tools) and run by the host +(`src/toolbox_peers.rs`, over `crates/toolbox`'s `peer` module). + +| The app's grant | Registered tools (risk) | +| --- | --- | +| none | the empty set (registration is mandatory) | +| `research` | `workflow.run` (read), `workflow.fork` (act), `toolbox.search` (read), `toolbox.web_read` (read) | +| `crawl`, with `max_depth` and `max_pages` above 0 in the scope | `toolbox.deep_crawl` (read) | + +octos's `deep_research` is never offered; `workflow.list` and +`workflow.evaluate` are not either (the run tool's description lists the +templates). All are `background`, none `outward`, `confirm: host`: none is +gated by the kernel. A call is checked again against the grant (a forged +`toolbox.deep_crawl` is refused), runs with the app's `AppContext` (id, +grants, octos `Scope`, its own budget) on the octos research engine, and its +model calls go through the `model` service's `ModelHost::complete`: the +person's providers and the app's daily budget, in the same ledger as +`model.complete`. + +Results are written to the host-owned `/.host/toolbox/` +(`toolbox_folder`, always compiled; run results under +`toolbox/runs/