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
16 changes: 11 additions & 5 deletions crates/kernel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,11 +167,17 @@ file tools (octos fences them to the session's working directory), memory,
`ask_user_question`, media viewing, octos's `web_search` / `web_fetch` (until
toolbox grants replace them, #108) and `tool_search`. Granted toolbox and
cross-app tools join it as host tools through `SystemAgentTools`. Command
execution is **planned, not available**: it is to be a host tool with a live
approval (`terminal.run`), off by default and turned on by a Settings
switch. `SystemAgentTools::grant_command_execution` is the seam, but no
Settings switch sets it yet, and the host tool cannot be registered on the
system session until octos can register tools per session (octos#2567).
execution is **done** as a grant: the person's switch in Setup → Assistant →
Command execution (off by default; turning it on needs the confirmation the
person types, which says what it risks; `crates/shell/src/system_chat/grants.rs`)
gives the system agent the host tool `terminal.run` through
`SystemAgentTools::grant_command_execution`. The shell hands the grants to
this crate (`system_tools::set_grants`); each kernel start takes them
(`grants_at_start`, `system_agent_tools_in_effect()`), so a change applies
after a restart, which Settings offers. Each command goes through the shell's
approval router as `auto_approvable: false` with a live sheet showing the exact
command (developer mode still answers it). Registering the host tool on the
system session waits for octos's per-session tool registration (octos#2567).

**What the kernel enforces today.** octos has no tool list the host can set
for one session, so every start writes the `_main` profile's `tool_policy`
Expand Down
8 changes: 5 additions & 3 deletions crates/kernel/README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,11 @@ adb -s SERIAL forward tcp:PORT tcp:PORT
[ADR 0004](../../docs/adr/0004-native-apps-hosting-and-peers.md) §12:系统智能体的工具集就是它获得的授权。它默认的 octos
工具是 `system_tools::SYSTEM_AGENT_TOOLS`:监督(`peer_send_input`、`peer_gather`、`peer_list`、`peer_respond`、
`peer_close`)、其工作区内的文件工具(octos 将其限制在会话工作目录内)、记忆、`ask_user_question`、查看媒体、octos 的
`web_search` / `web_fetch`(在工具箱授权取代它们之前,#108)以及 `tool_search`。授予的工具箱工具和跨应用工具通过 `SystemAgentTools` 作为宿主工具加入。命令执行**尚在计划中,目前不可用**:
它将是需要实时批准的宿主工具(`terminal.run`),默认关闭,由设置中的开关开启。`SystemAgentTools::grant_command_execution`
是预留接口,但目前还没有设置开关调用它,而且在 octos 支持按会话注册工具(octos#2567)之前,无法在系统会话上注册该宿主工具。
`web_search` / `web_fetch`(在工具箱授权取代它们之前,#108)以及 `tool_search`。授予的工具箱工具和跨应用工具通过 `SystemAgentTools` 作为宿主工具加入。命令执行作为授权**已完成**:用户在“设置 → 助手 → 命令执行”中的开关(默认关闭;开启需要用户输入确认语,确认语说明其风险;
`crates/shell/src/system_chat/grants.rs`)通过 `SystemAgentTools::grant_command_execution` 为系统代理授予宿主工具 `terminal.run`。
Shell 把授权交给本 crate(`system_tools::set_grants`);每次内核启动时采用(`grants_at_start`、`system_agent_tools_in_effect()`),
因此更改在重启后生效,设置中提供重启按钮。每条命令都经过 Shell 的批准路由器,按 `auto_approvable: false` 处理,并在实时批准表单上显示完整命令(开发者模式仍可直接批准)。
在 octos 支持按会话注册工具(octos#2567)之前,该宿主工具尚无法注册到系统会话上。

**内核今天执行的内容。** octos 没有宿主可为单个会话设置的工具名单,因此每次启动都写入 `_main` profile 的 `tool_policy`
(`system_tools::tool_policy`):任何授权可给予的一切,唯独去掉 octos 自己的 shell(`group:runtime`:`shell`、`bash`、
Expand Down
3 changes: 3 additions & 0 deletions crates/kernel/src/launch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,9 @@ pub(crate) fn prepare(launch: &Launch, core_dir: &Path) {
// Every start: the system agent's tool set (ADR 0004 §12) as the
// profile's tool policy, which octos reads at start.
crate::system_tools::enforce(core_dir);
// ... and the grants it starts with (a Settings change applies from the
// next start: the shell offers a restart).
crate::system_tools::take_grants_for_start();
}

/// Floor for `memory.max_inject_tokens` in the phone kernel's config.
Expand Down
10 changes: 10 additions & 0 deletions crates/kernel/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,16 @@ pub fn status() -> Status {
global().status()
}

/// The system agent's grants the running kernel started with, or `None`
/// when no kernel runs (then the next start takes
/// [`system_tools::grants`]).
pub fn system_agent_tools_in_effect() -> Option<system_tools::SystemAgentTools> {
if !status().running {
return None;
}
system_tools::grants_at_start()
}

/// `<core_dir>/profiles/_main.json` of the process's kernel.
pub fn profile() -> Option<PathBuf> {
core_dir().map(|d| profile_path(&d))
Expand Down
60 changes: 57 additions & 3 deletions crates/kernel/src/system_tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,11 @@ impl SystemAgentTools {
/// the host tool [`COMMAND_EXECUTION_TOOL`], each command approved live
/// (section 8); never octos's shell.
///
/// TODO(ADR 0004 plan steps 4/6): persist the switch in Settings →
/// Assistant (no Settings plumbing for it exists yet) and register the
/// host tool on the system session once octos can (octos#2567 item 6).
/// The shell persists the switch (Setup → Assistant → Command
/// execution, `crates/shell/src/system_chat/grants.rs`) and hands the
/// set to [`set_grants`]; a kernel start takes it ([`grants_at_start`]).
/// TODO(ADR 0004 plan step 6): register the host tool on the system
/// session once octos can (octos#2567 item 6).
pub fn grant_command_execution(&mut self, on: bool) -> &mut Self {
self.command_execution = on;
self
Expand All @@ -209,6 +211,44 @@ impl SystemAgentTools {
}
}

// ---- the grants a kernel starts with ---------------------------------------

/// The person's current grants for the system agent (the shell sets them
/// from Settings), and the grants the running kernel generation started
/// with: a change applies from the next kernel start.
static GRANTS: std::sync::Mutex<Option<SystemAgentTools>> = std::sync::Mutex::new(None);
static AT_START: std::sync::Mutex<Option<SystemAgentTools>> = std::sync::Mutex::new(None);

fn lock<T>(m: &std::sync::Mutex<T>) -> std::sync::MutexGuard<'_, T> {
m.lock().unwrap_or_else(|e| e.into_inner())
}

/// The shell hands over the person's grants (at startup, and whenever a
/// Settings switch changes). Only the shell calls this, from its Settings
/// handler, which requires the person's gesture to turn a grant on.
pub fn set_grants(tools: SystemAgentTools) {
*lock(&GRANTS) = Some(tools);
}

/// The person's current grants ([`SystemAgentTools::new`] until the shell
/// sets them).
pub fn grants() -> SystemAgentTools {
lock(&GRANTS).clone().unwrap_or_default()
}

/// Called before every kernel start (`launch::prepare`): the new
/// generation runs with the grants of this moment.
pub(crate) fn take_grants_for_start() {
*lock(&AT_START) = Some(grants());
}

/// The grants the last kernel start took; `None` before any start. The
/// shell compares it with [`grants`] to say "restart the assistant to
/// apply" (see `crate::system_agent_tools_in_effect`).
pub fn grants_at_start() -> Option<SystemAgentTools> {
lock(&AT_START).clone()
}

/// The octos `ToolPolicy` the kernel runs `_main` with: everything a grant
/// can give (an empty allowlist is octos's "allow all") except octos's
/// shell, marked as OctoSense's.
Expand Down Expand Up @@ -325,6 +365,20 @@ mod tests {
assert!(!granted.names().contains("shell"), "never octos's shell");
}

#[test]
fn a_start_takes_the_grants_of_that_moment() {
let mut on = SystemAgentTools::new();
on.grant_command_execution(true);
set_grants(on.clone());
take_grants_for_start();
assert_eq!(grants_at_start(), Some(on));
// A later change waits for the next start.
set_grants(SystemAgentTools::new());
assert!(grants_at_start().unwrap().command_execution());
take_grants_for_start();
assert!(!grants_at_start().unwrap().command_execution());
}

#[test]
fn enforce_writes_ours_keeps_the_rest_and_replaces_only_our_own() {
let dir = tmp("keep");
Expand Down
8 changes: 8 additions & 0 deletions crates/shell/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ fn main() {
if feature || target_os == "android" || target_env == "ohos" {
println!("cargo:rustc-cfg=mobile_only");
}
// `kernel`: this build hosts the octos kernel, exactly as
// octosense-ai-host's build.rs decides it (feature `octos-core`, or a
// native mobile target): `octosense_ai_host::kernel` exists. The system
// chat (src/system_chat/) reads it.
println!("cargo:rustc-check-cfg=cfg(kernel)");
if native_mobile || std::env::var_os("CARGO_FEATURE_OCTOS_CORE").is_some() {
println!("cargo:rustc-cfg=kernel");
}
// `makepad_vulkan`: Makepad renders with Vulkan (`MAKEPAD=vulkan`, the
// same switch makepad-platform's build reads), so a Linux desktop can
// share a process app's frames zero-copy (native-apps.json's
Expand Down
18 changes: 16 additions & 2 deletions crates/shell/src/approvals/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ pub struct Approvals {
/// Decisions for the peer link's calls (`peerlink:` ids), drained by
/// `peer_link` ([`take_peer_decisions`]).
peer: RecordingRelay,
/// Decisions for the system chat's approvals (`syschat:` ids), drained
/// by the chat ([`take_system_chat_decisions`]).
system_chat: RecordingRelay,
external: Arc<Mutex<Option<Box<dyn ApprovalRelay>>>>,
}

Expand All @@ -74,6 +77,7 @@ pub struct Approvals {
struct Dispatch {
bus: RecordingRelay,
peer: RecordingRelay,
system_chat: RecordingRelay,
queue: RecordingRelay,
external: Arc<Mutex<Option<Box<dyn ApprovalRelay>>>>,
}
Expand All @@ -86,6 +90,9 @@ impl ApprovalRelay for Dispatch {
if id.0.starts_with(crate::peer_link::link::HELD_PREFIX) {
return self.peer.approval_decided(id, decision, reason);
}
if id.0.starts_with(crate::system_chat::HELD_PREFIX) {
return self.system_chat.approval_decided(id, decision, reason);
}
match self.external.lock().unwrap_or_else(|e| e.into_inner()).as_mut() {
Some(relay) => relay.approval_decided(id, decision, reason),
None => self.queue.approval_decided(id, decision, reason),
Expand All @@ -105,10 +112,11 @@ impl Approvals {
let queue = RecordingRelay::default();
let bus = RecordingRelay::default();
let peer = RecordingRelay::default();
let system_chat = RecordingRelay::default();
let external = Arc::new(Mutex::new(None));
let dispatch = Dispatch { bus: bus.clone(), peer: peer.clone(), queue: queue.clone(), external: external.clone() };
let dispatch = Dispatch { bus: bus.clone(), peer: peer.clone(), system_chat: system_chat.clone(), queue: queue.clone(), external: external.clone() };
let router = Router::new(rules, audit, Box::new(dev_hooks::ShellDevMode), contacts, Box::new(dispatch));
Approvals { router, consent, settings_open: false, queue, bus, peer, external }
Approvals { router, consent, settings_open: false, queue, bus, peer, system_chat, external }
}
/// Sheets, rules, consent and the page: one number for "redraw".
pub fn generation(&self) -> u64 {
Expand Down Expand Up @@ -187,6 +195,12 @@ pub fn take_peer_decisions() -> Vec<(RequestId, Decision, String)> {
with(|a| a.peer.take()).unwrap_or_default()
}

/// The system chat's approvals (`crate::system_chat`): the router's
/// decisions, which the chat alone sends to the kernel.
pub fn take_system_chat_decisions() -> Vec<(RequestId, Decision, String)> {
with(|a| a.system_chat.take()).unwrap_or_default()
}

/// An app module registers its own confirmation sheet (`confirm: app`).
pub fn register_app_confirm(app: &str, handler: Box<dyn AppConfirm>) {
with(|a| a.router.register_app_confirm(app, handler));
Expand Down
2 changes: 1 addition & 1 deletion crates/shell/src/approvals/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,7 @@ fn only_settings_and_the_sheet_make_a_person_gesture() {
let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let mut files = Vec::new();
walk(&src, &mut files);
let allowed = ["approvals/view.rs", "approvals/settings_page.rs", "approvals/rules.rs", "approvals/tests.rs"];
let allowed = ["approvals/view.rs", "approvals/settings_page.rs", "approvals/rules.rs", "approvals/tests.rs", "system_chat/tests.rs"];
for f in files {
let text = std::fs::read_to_string(&f).unwrap();
let rel = f.strip_prefix(&src).unwrap().to_string_lossy().replace('\\', "/");
Expand Down
Loading