Skip to content
Merged
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
8 changes: 4 additions & 4 deletions Cargo.lock

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

19 changes: 15 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ octosense-reference = { path = "apps/reference" }
makepad-sheets = { git = "https://github.com/OctoSense-org/makepad.git", rev = "92a19b67218f8cd2361866c4554457b153766f48", default-features = false }
makepad-terminal = { git = "https://github.com/OctoSense-org/makepad.git", rev = "92a19b67218f8cd2361866c4554457b153766f48" }
octosense-appcard = { path = "apps/appcard/module" }
octosense-app-hub-app = { git = "https://github.com/OctoSense-org/OctoSense-App-Hub", rev = "e8601b80ce104db2e48208094714bdcffdce6b5a" }
octosense-app-hub-app = { git = "https://github.com/OctoSense-org/OctoSense-App-Hub", rev = "8512be70b89b366e6d6ebf93e807188c4313c3c0" }
# END native-apps: pins
# --- Makepad (OctoSense-org/makepad), the revision Octoscript-Makepad's
# runtime.json pins (native-runtime.lock.json); resolved to .sources/makepad.
Expand All @@ -93,12 +93,12 @@ octoscript-makepad = { git = "https://github.com/OctoSense-org/Octoscript-Makepa
octoscript-node = { git = "https://github.com/OctoSense-org/Octoscript-Makepad.git", rev = "ecab2b985a6f60376683cf7317dc82579393558f" }
# --- App Hub's host service registry the Mail and llm services register
# into; the same revision as App Hub's app crate (a native app, above).
octosense-appstore = { git = "https://github.com/OctoSense-org/OctoSense-App-Hub", rev = "e8601b80ce104db2e48208094714bdcffdce6b5a" }
octosense-appstore = { git = "https://github.com/OctoSense-org/OctoSense-App-Hub", rev = "8512be70b89b366e6d6ebf93e807188c4313c3c0" }
# The app policy (resolution, isolate settings, the Splash adapter) and the
# Hub client the Card runner resolves an app's policy with; the shell's
# glance tiles run cards under it (crates/shell/src/glance_card.rs).
octosense-app-policy = { git = "https://github.com/OctoSense-org/OctoSense-App-Hub", rev = "e8601b80ce104db2e48208094714bdcffdce6b5a" }
octosense-app-hub = { git = "https://github.com/OctoSense-org/OctoSense-App-Hub", rev = "e8601b80ce104db2e48208094714bdcffdce6b5a" }
octosense-app-policy = { git = "https://github.com/OctoSense-org/OctoSense-App-Hub", rev = "8512be70b89b366e6d6ebf93e807188c4313c3c0" }
octosense-app-hub = { git = "https://github.com/OctoSense-org/OctoSense-App-Hub", rev = "8512be70b89b366e6d6ebf93e807188c4313c3c0" }
# --- octos: ONE source for every octos crate (octos-org/octos main at the
# merge of octos#2626, the shared peer conversation with turn origin; after
# #2621, `peer/input/reject`, and #2616, the #2567 follow-ups).
Expand Down Expand Up @@ -243,6 +243,17 @@ octosense-app-peers = { path = "crates/app-peers" }
octosense-kernel = { path = "crates/kernel" }
octosense-llm-config = { path = "apps/ai-providers/config" }

# One App Hub: Rinx v1.0.0 names App Hub at an older rev (e8601b80) than the
# pin above (its mini-app catalog and system apps take app-policy and the
# hub client). A patch cannot point a source at another rev of itself, so
# every App Hub crate resolves through the www host alias of the same
# repository, at the pin. Drop this once Rinx pins the same App Hub commit.
[patch."https://github.com/OctoSense-org/OctoSense-App-Hub"]
octosense-app-hub = { git = "https://www.github.com/OctoSense-org/OctoSense-App-Hub", rev = "8512be70b89b366e6d6ebf93e807188c4313c3c0" }
octosense-app-policy = { git = "https://www.github.com/OctoSense-org/OctoSense-App-Hub", rev = "8512be70b89b366e6d6ebf93e807188c4313c3c0" }
octosense-appstore = { git = "https://www.github.com/OctoSense-org/OctoSense-App-Hub", rev = "8512be70b89b366e6d6ebf93e807188c4313c3c0" }
octosense-app-hub-app = { git = "https://www.github.com/OctoSense-org/OctoSense-App-Hub", rev = "8512be70b89b366e6d6ebf93e807188c4313c3c0" }

# Rinx's Matrix stack: the patched ruma its own workspace uses (Rinx's
# [patch] sections do not reach a consumer workspace).
[patch."https://github.com/ruma/ruma"]
Expand Down
11 changes: 11 additions & 0 deletions apps/news/bundle/manifest.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
{
"agent": {
"model": {
"needs": [
"tool_calling"
]
},
"profile": "read-only",
"tools": [
"ask_user_question"
]
},
"capabilities": [
"storage",
"net",
Expand Down
13 changes: 13 additions & 0 deletions crates/shell/src/app_chat/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,19 @@ pub fn snapshot() -> ChatModel {
model
}

/// Tests that drive the kernel themselves (the two-lane scenario): the
/// panel is open for `app` without a conversation of its own, so it shows
/// the app's questions and answers them as the person would here.
#[cfg(test)]
pub(crate) fn show_for_tests(app: AgentApp) {
with(|p| {
p.app = Some(app);
p.open = true;
p.focused = true;
p.ui_generation += 1;
});
}

pub fn status() -> Status {
lock(&shared()).status.clone()
}
Expand Down
5 changes: 5 additions & 0 deletions crates/shell/src/approvals/consent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ impl AgentSummary {
if uses.is_empty() {
uses.push(format!("{name}'s own tools"));
}
// A script app's agent that keeps App Hub's one kernel tool for
// contained apps (`ask_user_question`), in the store's words.
if manifest["agent"]["tools"].as_array().is_some_and(|t| t.iter().any(|t| t == "ask_user_question")) {
uses.push("Ask you questions".to_string());
}
AgentSummary { app: app.into(), name: name.into(), reads, uses, model: model.into() }
}
}
Expand Down
11 changes: 11 additions & 0 deletions crates/shell/src/approvals/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,17 @@ fn news() -> AgentSummary {
)
}

/// A script app's agent that keeps `ask_user_question` says so on the
/// first-use sheet, in the store's words, beside its own tools.
#[test]
fn the_consent_sheet_says_a_script_agent_asks_questions() {
let manifest = json!({"capabilities": ["news"], "agent": {"profile": "read-only", "tools": ["ask_user_question"]}});
let summary = AgentSummary::from_manifest("os.news", "News", &manifest, &[], "m");
assert_eq!(summary.uses, ["News's own tools", "Ask you questions"]);
let quiet = AgentSummary::from_manifest("os.news", "News", &json!({"capabilities": ["news"]}), &[], "m");
assert_eq!(quiet.uses, ["News's own tools"]);
}

/// The consent sheet and app storage read one `storage` block the same
/// way: `accounts` defaults to false (one `device` folder) in both.
#[test]
Expand Down
43 changes: 28 additions & 15 deletions crates/shell/src/host_tools/scenario_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@
//! destructive and outward tools alike); both run on a
//! recording `news` host service through the real script-app executor
//! (`script_apps::HostServiceExecutor`), wrapped to count the calls it
//! runs. Its agent keeps octos's `ask_user_question`, granted through the
//! relay's `set_generic` seam (native apps' `agent.generic_tools`): App Hub
//! offers contained apps no octos kernel tool, so an admitted manifest
//! cannot ask for it.
//! runs. Its agent keeps octos's `ask_user_question`, from its manifest's
//! `agent.tools` (App Hub's one kernel tool for a contained app), through the
//! script-app path (G3) into the peer's `generic_tools`; its person-lane
//! question is shown and answered in the shell's "Ask News" panel.
//!
//! **The lanes.** The system agent's lane is a real `peer/input`: a turn on
//! the system session makes the model call `peer_send_input`, the kernel
Expand Down Expand Up @@ -172,7 +172,8 @@ fn register_app(dir: &Path) {
let manifest = json!({
"schema": 1, "id": APP, "version": "1", "name": LABEL,
"integrity": {"bundle_blake3": digest},
"capabilities": ["storage", "glance", "news"]
"capabilities": ["storage", "glance", "news"],
"agent": {"profile": "read-only", "tools": ["ask_user_question"], "model": {"needs": ["tool_calling"]}}
});
std::fs::write(bundle.join("manifest.json"), serde_json::to_vec_pretty(&manifest).unwrap()).unwrap();
let pack = serde_json::to_string(&octosense_app_hub::pack::pack_dir(&bundle).unwrap()).unwrap();
Expand Down Expand Up @@ -297,13 +298,11 @@ impl Scenario {
let system = octosense_appstore::system::system_app(APP).unwrap();
let bundle = octosense_appstore::system::prepare(&root, &system).expect("the fixture unpacks").0;
let loaded = super::script_apps::from_bundle(&bundle).unwrap();
// App Hub offers a contained app's agent no octos kernel tool
// (`HostLimits::offered_tools`: ledger, net, storage, card), so an
// admitted manifest cannot ask for `ask_user_question`. The fixture's
// agent keeps it through the relay's seam native apps use
// (`agent.generic_tools`).
assert!(loaded.generic.is_empty());
super::set_generic(APP, vec!["ask_user_question".into()]);
// Its agent keeps `ask_user_question` from its manifest: App Hub
// admits that one kernel tool for a contained app, and the
// script-app path hands it to the peer's `generic_tools`.
assert_eq!(loaded.generic, ["ask_user_question"]);
assert_eq!(super::with_relay(|r| r.catalog.generic(APP, false)), ["ask_user_question"]);
let executed: Arc<Mutex<Vec<HostToolCall>>> = Arc::default();
let inner = super::script_apps::HostServiceExecutor { app: APP.into(), tools: loaded.host_service_tools, families: loaded.families, host_dir: root.join(".host") };
super::set_executor(APP, Some(Arc::new(Counting { inner: Arc::new(inner), ran: executed.clone() })));
Expand Down Expand Up @@ -561,9 +560,23 @@ fn the_system_agent_and_the_person_work_with_one_app_agent_at_once() {
let asked = requests.iter().find(|r| r["own"].as_str().is_some_and(|u| u.contains("SCN_ASK"))).expect("the person's model request");
eprintln!("[main] the person's lane was shown, while the system agent's turn ran: {}", asked["shared"]);

// 4. The person answers on the shell's question card: their turn ends
// while the system agent's is still parked on its approval.
press(Hit::QuestionOption { id: question.id, label: Some("Team".into()) });
// 4. The person answers in the shell's "Ask News" panel (G11: the
// question of the app's conversation is shown there, not in the system
// chat): their turn ends while the system agent's is still parked on
// its approval.
crate::app_chat::show_for_tests(crate::apps::AgentApp { id: APP.into(), name: LABEL.into(), octos: Vec::new(), manifest: json!({}), native: false });
let panel = crate::app_chat::snapshot();
let routed = panel
.items
.iter()
.find_map(|i| match i {
crate::system_chat::model::Item::Question { id, body, options, answered: None, .. } if body == "Who should see the summary?" => Some((id.clone(), options.clone())),
_ => None,
})
.unwrap_or_else(|| panic!("the panel shows the question: {:?}", panel.items));
assert_eq!(routed, (format!("{}{}", crate::app_chat::ROUTED_PREFIX, question.id), vec!["Team".to_string(), "Everyone".to_string()]));
crate::app_chat::answer_option(&routed.0, 1, "Team");
crate::app_chat::close();
let said = wait_complete(&s, &person, "the person's turn").expect("the person's turn completed");
assert!(said["text"].as_str().unwrap_or("").starts_with("SCN AUDIENCE") && said["text"].to_string().contains("Team"), "{said}");
assert_eq!(said["lane"], "person");
Expand Down
43 changes: 25 additions & 18 deletions crates/shell/src/host_tools/script_apps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
//! match its manifest, and every rule the store's gate applies holds), from
//! the unpacked system app (`octosense_appstore::system::prepare`) or the
//! installed bundle (`<apps root>/<id>/bundle`). The manifest's
//! `agent.tools` names both kinds of grant: a plain name (`web_search`) is an
//! octos kernel tool its agent keeps (`generic_tools`; never octos's
//! shell), a dotted one (`mail.send`) another app's shareable tool, granted
//! at install and marked with its owner.
//! `agent.tools` names both kinds of grant: a plain name is an octos kernel
//! tool its agent keeps (`generic_tools`: only App Hub's `KERNEL_TOOLS`,
//! `ask_user_question`, which App Hub alone admits), a dotted one
//! (`mail.send`) another app's shareable tool, granted at install and
//! marked with its owner.
//!
//! **Where the calls go** ([`HostServiceExecutor`]). A tool the bundle says
//! is `implemented_by: "host-service"` runs on the host service of its
Expand Down Expand Up @@ -83,13 +84,11 @@ pub fn from_bundle(bundle: &Path) -> Result<Loaded, String> {
loaded.host_service_tools.insert(tool.name.clone());
}
}
for name in &agent.generic_tools {
if name.contains('.') {
loaded.asks.push(name.clone());
} else if !super::relay::OCTOS_SHELL.contains(&name.as_str()) {
loaded.generic.push(name.clone());
}
}
// Dotted names are other apps' tools. Of the kernel's own, App Hub
// lets a contained agent keep only `KERNEL_TOOLS` (`ask_user_question`)
// and refuses the rest at admission; `kernel_tools` is exactly those.
loaded.asks = agent.generic_tools.iter().filter(|name| name.contains('.')).cloned().collect();
loaded.generic = agent.kernel_tools().into_iter().filter(|name| !super::relay::OCTOS_SHELL.contains(&name.as_str())).collect();
Ok(loaded)
}

Expand Down Expand Up @@ -245,27 +244,35 @@ pub(crate) mod tests {
assert!(loaded.tools.iter().all(|t| t["risk"] == "read" && t["shareable"] == true && t.get("implemented_by").is_none()));
assert_eq!(loaded.host_service_tools.len(), 2);
assert!(loaded.families.contains("news"), "News is granted its service");
assert!(loaded.generic.is_empty(), "no agent block: no kernel tools");
assert_eq!(loaded.generic, ["ask_user_question"], "News's agent may ask the person");
// A tampered bundle is refused (App Hub's digest check).
std::fs::write(dir.join("tools.json"), "{}").unwrap();
assert!(from_bundle(&dir).is_err());
let _ = std::fs::remove_dir_all(dir);
}

/// The manifest's `agent.tools`: plain names are kernel tools (never
/// octos's shell), dotted ones other apps' tools, granted with their
/// owner.
/// The manifest's `agent.tools`: a plain name is a kernel tool, and App
/// Hub admits only `ask_user_question` (any other, `web_search` or
/// octos's shell, refuses the bundle); dotted ones are other apps'
/// tools, granted with their owner.
#[test]
fn a_script_apps_agent_block_splits_kernel_tools_from_other_apps_tools() {
let dir = stamped_bundle("news", "agent", |_, m| {
m["agent"] = json!({"profile": "read-only", "tools": ["web_search", "ask_user_question", "shell", "mail.send"],
"model": {"needs": ["tool_calling"]}});
m["agent"] = json!({"profile": "read-only", "tools": ["ask_user_question", "mail.send"], "model": {"needs": ["tool_calling"]}});
});
let loaded = from_bundle(&dir).unwrap();
assert_eq!(loaded.generic, ["web_search", "ask_user_question"]);
assert_eq!(loaded.generic, ["ask_user_question"]);
assert_eq!(loaded.asks, ["mail.send"]);
assert_eq!(owner_for("mail.send"), "os.mail", "the system app of its namespace until someone declares it");
let _ = std::fs::remove_dir_all(dir);
for tool in ["web_search", "shell", "read_file"] {
let dir = stamped_bundle("news", tool, |_, m| {
m["agent"] = json!({"profile": "read-only", "tools": ["ask_user_question", tool], "model": {"needs": ["tool_calling"]}});
});
let refused = from_bundle(&dir).unwrap_err();
assert!(refused.contains("may keep only ask_user_question"), "{tool}: {refused}");
let _ = std::fs::remove_dir_all(dir);
}
}

struct Probe;
Expand Down
1 change: 1 addition & 0 deletions docs/adr/0004-native-apps-hosting-and-peers.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,7 @@ How it is turned on and kept from leaking into normal use:
### Follow-ups

- [x] **An agent for every app that declares one** (section 4, 2026-09-29): a live run on `625d6cc` found News (tools.json, no `octos.*`) without a peer, an empty `peer_list` for the system agent, Settings listing only Rinx, and no place for the person to talk to an app's agent. Now the shell lists every app with an agent, prepares a consented app's peer, tells the system agent about the ones it cannot list, and draws "Ask <app>". Still open: a native app's peer exists only while the app is open; the speaker label of a script app's person turns is its app id (the peer's name, kept for resume), not its display name; prepared peers hold a kernel connection, so the kernel's idle stop waits for them.
- [x] **The "Ask <app>" panel's live run, follow-ups** (2026-09-29, [#189](https://github.com/OctoSense-org/OctoSense/pull/189) and the App Hub pin after it): the panel orders both lanes by one clock (each lane's `cursor.seq` is its own; a turn's request line stands where the turn started, handed over by the broker with `turn/started`, since octos writes a turn's user message only when it ends and never for a stopped turn); Stop says "interrupted" once; the system agent is given each peer's slug for `peer_send_input`; and a script app's agent may keep octos's `ask_user_question` (App Hub's one kernel tool for contained apps, `KERNEL_TOOLS`, "Ask you questions" in the store), handed to its peer's `generic_tools` by G3. News's agent keeps it, and its questions of the person's lane are asked in "Ask News".

*Still open after the implementation (2026-09-29):* the Windows AppContainer sandbox ([#137](https://github.com/OctoSense-org/OctoSense/issues/137)); the Linux sandbox has run only in CI; process apps see the system agent's lane only through history until Makepad's peer client reads `conversation` frames; a second message on one conversation handle is refused rather than queued; Rinx still has to send turn triggers and hand over its send sheet; the OpenHarmony build with octos#2619's BoringSSL (`btls-sys`) is untested.

Expand Down
2 changes: 1 addition & 1 deletion native-apps.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
"id": "apphub",
"feature": "app-hub",
"crate": "octosense-app-hub-app",
"source": { "git": "https://github.com/OctoSense-org/OctoSense-App-Hub", "rev": "e8601b80ce104db2e48208094714bdcffdce6b5a", "local": null },
"source": { "git": "https://github.com/OctoSense-org/OctoSense-App-Hub", "rev": "8512be70b89b366e6d6ebf93e807188c4313c3c0", "local": null },
"module": ["octosense_app_hub_app::APP_HUB_MODULE", "octosense_app_hub_app::CARD_MODULE"],
"bin": null,
"default_features": true,
Expand Down
Loading
Loading