Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/apps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 10 additions & 3 deletions apps/ai-providers/host-service/src/complete/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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()));
Expand Down
22 changes: 22 additions & 0 deletions apps/ai-providers/host-service/tests/complete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
21 changes: 21 additions & 0 deletions crates/ai-host/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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/*;
Expand All @@ -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 }
Expand All @@ -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`
Expand Down
50 changes: 47 additions & 3 deletions crates/ai-host/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<apps root>/.host/toolbox/<app id>`
(`toolbox_folder`, always compiled; run results under
`toolbox/runs/<template>/<run>.json`, research items under `research/`),
outside the app's jail, where the glance screen's `sys.digest` (OctoSense
#87) reads them.

**Grants.** `research`/`crawl` in the manifest's `capabilities`, the scope
in octos's `Scope` shape under the manifest's top-level `research` object
(App Hub #26's shape). **Temporary:** until the shells' App Hub pin includes
#26 and the host reads its verified grant, only system apps (`os.*`) get
what they declare; any other app's declaration is ignored.

**Turning it on** needs octos#2567 on the workspace's octos pin: a kernel
without it refuses `peer/tools/register`, and the broker then starts no
turns for any app, Rinx included. It also changes what a registered peer's
turns get (see `crates/app-peers`'s README).

Tests: `cargo test -p octosense-ai-host --features octos-core,llm` (and
without features for a kernel-less desktop). The module-host tests that
without features for a kernel-less desktop); `--features toolbox-peers` adds
the toolbox's registration and routing against a scripted kernel with the
toolbox's fixture backends, and, when `OCTOS_APP_PEERS_TEST_KERNEL` names an
`octos` built with octos#2567, `tests/toolbox_real_kernel.rs` against the
real kernel. The module-host tests that
create real instances (Rinx included) live with each shell's
`module_host.rs`.

Expand Down
43 changes: 43 additions & 0 deletions crates/ai-host/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,16 @@
//! declares or is granted nothing gets nothing, and no peer is allocated.
//! Dropping the [`Assistant`] releases the instance's leases and
//! interrupts its peer's running work; the kernel and other apps go on.
//! - **The system toolbox for app agents** (feature `toolbox-peers`, off by
//! default until octos#2567 is on the pin): every app peer registers its
//! toolbox tools with the kernel and the host runs their calls
//! ([`toolbox_peers`]). Its results live in each app's host-owned toolbox
//! folder ([`toolbox_folder`]), which exists with or without the feature.

mod bridge;
mod qr;
#[cfg(feature = "toolbox-peers")]
pub mod toolbox_peers;

pub use bridge::{Bridge, Done};
pub use qr::{ImageSource, PickError, QrImport, DROP_APP};
Expand Down Expand Up @@ -354,6 +361,32 @@ pub fn kernel_running() -> bool {
false
}

// ---- the system toolbox's folders ------------------------------------------

/// The host's folder for every app's toolbox, under the apps root:
/// `<apps root>/.host/toolbox`. Outside every app's jail
/// (`<apps root>/<app id>`), so an app cannot write a digest the glance
/// screen would show as the host's.
pub const TOOLBOX_HOST_DIR: [&str; 2] = [".host", "toolbox"];

/// `<apps root>/.host/toolbox` ([`TOOLBOX_HOST_DIR`]). The glance screen's
/// `sys.digest` resolver (OctoSense #87) reads run results under it.
pub fn toolbox_root(apps_root: &std::path::Path) -> PathBuf {
TOOLBOX_HOST_DIR.iter().fold(apps_root.to_path_buf(), |dir, part| dir.join(part))
}

/// One app's toolbox folder, `<apps root>/.host/toolbox/<app id>`: run
/// results in `toolbox/runs/<template>/<run>.json`, forks in
/// `toolbox/templates/`, research items in `research/`. `None` for an id
/// that is not one safe path segment.
pub fn toolbox_folder(apps_root: &std::path::Path, app_id: &str) -> Option<PathBuf> {
let valid = !app_id.is_empty()
&& app_id.len() <= 64
&& !app_id.starts_with('.')
&& app_id.bytes().all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-'));
valid.then(|| toolbox_root(apps_root).join(app_id))
}

// ---- apps' assistant access -------------------------------------------------

/// The host policy in force: [`Policy::shipped`] until [`start`] installs
Expand Down Expand Up @@ -442,12 +475,22 @@ pub fn offer(module: &dyn AppModule, scope: &InstanceScope) -> Offer {
let assistant = STATE.get().is_none_or(|s| s.kernel).then(|| {
// Before `start` (a module host's own tests) the kernel keeps its
// defaults; after it, only a shell that hosts a kernel offers.
#[cfg(not(feature = "toolbox-peers"))]
let broker = octosense_app_peers::hosted::launch(
module.id(),
module.label(),
module.capabilities().iter().copied(),
host_policy(),
)?;
// Every peer registers its toolbox tools, possibly none (octos#2567).
#[cfg(feature = "toolbox-peers")]
let broker = octosense_app_peers::hosted::launch_with_tools(
module.id(),
module.label(),
module.capabilities().iter().copied(),
host_policy(),
toolbox_peers::tools_for_module(module.id(), module.capabilities().iter().copied()),
)?;
octosense_app_peers::hosted::offer(module.id(), &scope, &broker);
Some(Assistant { broker })
}).flatten();
Expand Down
Loading
Loading