diff --git a/.github/workflows/apps.yml b/.github/workflows/apps.yml index 55f49f0e..eb686f44 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) @@ -144,6 +148,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 0f52e019..a1f598e2 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..733d26ad 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/