Skip to content

Latest commit

 

History

History
306 lines (264 loc) · 18.1 KB

README.md

File metadata and controls

306 lines (264 loc) · 18.1 KB

octosense-kernel: the shell's octos kernel

English | 简体中文

Where this fits. One octos kernel per shell: a child process on the desktop and Android, in process on OpenHarmony, none on iOS. The shell holds the host token and is the only host connection; the system agent and every app agent are sessions in this kernel, and a Talk to Octos client gets only the system conversation with the external token. Diagrams of the processes, an app agent's two lanes and a tool call with its approval: How it fits together; the details: docs/architecture.md and ADR 0004.

The octos agent kernel is a shell service. The shell (Home in phone/, the desktop in desktop/) owns it; the AI providers system app configures it through the llm host service; its consumers connect to it: the shell's system chat (the system agent's pane, crates/shell/src/system_chat/link.rs), every app agent's broker (crates/app-peers, CoreConnector: Rinx and the script apps with an agent) and the opt-in AppCard. This crate is that service: one kernel per process, started on demand, shared, restarted when the providers change.

It lives in crates/ rather than apps/ because it is not an app: it is the shared runtime piece the shells, AppCard (apps/appcard/app) and the llm service (apps/ai-providers/host-service, feature octos-core) link.

What it does

Core dir octos's data dir: <core_dir>/profiles/_main.json is the profile the AI providers app writes. Resolved as: the shell's Options::core_dir, else $OCTOS_APP_CORE_DIR, else <app data dir>/octos-home/.octos: OctoSense's own octos home (the platform's app data dir on a phone, the app-private home AppCard has always used; OctoSense's state dir ~/.octosense on a desktop), else $HOME/octos-home/.octos (octosense_llm_config::profile::default_core_dir(), for a consumer that names nothing). A desktop OctoSense used to share $HOME/octos-home/.octos with the person's standalone octos; on first use it now copies only the provider and model settings (llm, env_vars) from there into its own profile, and never writes, moves or deletes anything there.
One kernel, lazily The first connect() starts it; later ones share it. octos holds a single-writer lock on its data dir, so a second kernel on the same dir could not run anyway.
Shared by frames A Connection carries UI Protocol (JSON-RPC) frames exactly as octos serve --stdio speaks them. Each consumer uses its own request ids and receives the replies to its requests and the notifications of the sessions it named (a notification for a session nobody named goes to every consumer).
Restart restart() stops a running kernel (a no-op when none runs). Connections then end with CloseReason::Restarted; a consumer connects again, which starts a fresh kernel that reads the new profile. The next kernel starts only after the old one has exited and released its data dir.
Idle stop When the last connection is dropped the kernel stops, as AppCard's own child used to (not while Talk to Octos is on, below).
Talk to Octos Off by default. While the person has it on, the kernel is octos's host-owned loopback server and external clients can attach to the same kernel (below).
Shutdown shutdown() stops it and waits (5 s at most).

How it starts, per platform (src/launch.rs):

  • Android: <nativeLibraryDir>/liboctos.so serve --stdio, HOME=<core dir's parent> (the octos home), with AppCard's environment (OCTOS_SKILLS_PATH, OCTOS_OMIT_WORKSPACE_HINT, RUST_LOG, and in a development build only the launch extra makepad.OCTOS_PROXY as the proxy: feature dev-mode or a debug build, since any app can start Home with extras) and the kernel config's memory budget. The APK must bundle the kernel: MAKEPAD_ANDROID_EXTRA_LIBS=liboctos.so=<octos> (the shells' build scripts do it).
  • OpenHarmony: the canonical core in-process, octos_cli::embedded::serve_io(<octos home>, ..), on this crate's runtime with 8 MiB worker stacks (HAP native libraries may not exec).
  • Desktop: <program> serve --stdio --data-dir <core_dir> (plus --config <core_dir>/config.json when that file exists) with OCTOS_HOME=<core_dir>; the program is the shell's Options::program, $OCTOS_APP_CORE_BIN, or the packaged octos-kernel[.exe] beside the shell executable (in a macOS .app, Contents/MacOS or Contents/Resources), in that order. The packaged kernel runs only when its receipt octos-kernel.json (beside it, in the .app's Contents/Resources, or a Linux package's usr/lib/octosense) names the octos revision this build pins (build.rs reads it from Cargo.lock) and its SHA-256 matches; otherwise the connection fails with the reason. tools/kernel-artifact.py --host --stage <dir> builds and stages it; OCTOSENSE_KERNEL_ANY_REVISION=1 accepts another revision while developing. No PATH search, no working directory, and a developer's own octos serve is never touched.
  • iOS: no kernel.
  • Talk to Octos on (desktop and Android): the same command with --host 127.0.0.1 --host-managed instead of --stdio, and the listener this process keeps passed as descriptor 3 (--listen-fd 3, Unix).

The kernel and the frame pump run on the crate's own Tokio runtime, so a consumer may use any runtime or none.

Using it

A shell, once at startup, before the first consumer:

octosense_kernel::configure(
    octosense_kernel::Options::default().app_data_dir(cx.get_data_dir()),
);
// The llm service (feature `octos-core`) writes under the same core dir
// and calls octosense_kernel::restart() after every change.
octosense_llm_service::register_with(
    octosense_llm_service::Options::default().core_dir(octosense_kernel::core_dir().unwrap()),
);

A consumer:

let mut conn = octosense_kernel::connect()?;       // Err: no kernel here
conn.send(r#"{"jsonrpc":"2.0","id":"1","method":"session/open","params":{"session_id":"_main:api:x","profile_id":"_main"}}"#)?;
loop {
    match conn.recv().await {
        Ok(frame) => { /* a JSON-RPC frame for this consumer */ }
        Err(octosense_kernel::CloseReason::Restarted) => { /* connect again, re-open sessions */ break }
        Err(other) => { /* the kernel stopped or could not start: tell the person */ break }
    }
}

AppCard's transport (apps/appcard/app/crates/octos-app-transport, kernel.rs) is the reference consumer: on Restarted it fails the requests still waiting, reconnects and opens its sessions again from their replay cursors, so the app carries on.

Other consumers. Each takes its own connection (connect()) and gets only its sessions' traffic: the system chat opens _main:api:octosense#system; an app-peers broker drives its app's peer and request contexts (Rinx's mini apps use request contexts of Rinx's peer through OctosAppService; each context has its own kernel session/transcript, but is not another app peer). Each must handle CloseReason::Restarted by reconnecting.

Other functions: core_dir(), home(), profile(), launch() / is_available() (whether and how a kernel would start), status(), and the Talk to Octos controls below.

Talk to Octos

Talk to Octos lets a web client or a terminal UI talk to this device's assistant. It is off by default; the kernel is then the private stdio child above and nothing listens. AI providers → Talk to Octos turns it on (set_external_access(true)), which:

  • restarts the kernel as octos serve --host-managed (octos docs/HOST_MANAGED_SERVE.md); native consumers keep the same frames over its WebSocket with a host token that never leaves this process (the kernel gets both tokens on its stdin, never in its environment), and request octos's stdio feature set (octos_core::ui_protocol::UI_PROTOCOL_STDIO_DEFAULT_FEATURES);
  • mints an external token. It opens /api/ui-protocol/ws and nothing else, and there only an allowlist of session, turn, answer and read-only status methods: no configuration (providers, keys, skills, snapshots), no server/shutdown, nothing on an app's assistant's sessions (host-owned app peers), and answers only in sessions it opened. Its turns get no tool that runs code, administers octos or reaches peers (octos UPCR-2026-036);
  • keeps the listener in this process (Unix) and hands it to every kernel generation, so a restart keeps the port and no other app can take it in between. Elsewhere a restart reuses the port when it is free, and otherwise moves to a new port with a new external token;
  • keeps the server up when native consumers leave, until it is turned off or the shell exits. The kernel's stdin is its lifeline: when the shell exits or crashes, the kernel sees EOF and stops.

How clients get in:

  • Web. The sheet's Pair a web client enables octos's pairing (pairing()): an 8-character code, valid for five minutes and one claim, shown with a QR of the web client's link (<web origin>/?octos=<server>&pair=<code>). The code only works while that sheet is open (end_pairing() when it closes). The web origin saved on the sheet is the only browser origin the server trusts: https, or on a desktop also http for localhost, 127.0.0.1 or [::1] (Android: https only). A malformed saved origin counts as none; the kernel still starts.
  • Terminal. The connection file connection_file(core_dir) (<core_dir>/client-connection.json, mode 0600; on Windows %LOCALAPPDATA%\OctoSense\client-connection.json, whose default ACL admits this user, SYSTEM and administrators) holds the endpoint and the external token for a client of this user. It is rewritten when the port or token changes and removed when the server stops. Terminal UI launch is unverified.
  • Revoke all clients (rotate_external_access()) mints a new external token and restarts the server; Turn off (set_external_access(false)) stops it, forgets the token and port, and removes the connection file.

A computer reaches a phone's server through a tunnel that keeps the port number, since the server only answers requests whose Host names its own port (unverified on a device):

adb -s SERIAL forward tcp:PORT tcp:PORT

The system conversation is _main:api:octosense#system in profile _main; its workspace is saved in system-workspace.txt so native opens and Web's scoped session agree. OpenHarmony (embedded core) and iOS (no kernel) have no Talk to Octos. See ADR 0003 for the threat model.

The system agent's tools

ADR 0004 §12: the system agent's tool set is its grants. Its default octos tools are system_tools::SYSTEM_AGENT_TOOLS: supervision (peer_send_input, peer_gather, peer_list, peer_respond; never peer_close, which the profile's tool_policy denies to every agent, since octos cannot resume a closed peer), its workspace's file tools (octos fences them to the session's working directory), memory, ask_user_question, media viewing (view_image, view_video), octos's web_search / web_fetch and tool_search. Toolbox and cross-app tools are meant to join it as host tools through SystemAgentTools (grant_toolbox, grant_cross_app), but nothing outside the tests grants them yet, so the system agent has none. The system chat always registers two host tools of its own on the session, agents.list and agents.ask (crates/shell/src/agents.rs: which apps have an agent, and the first-use sheet that allows one; only the person answers it). Command 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). While the switch is on, the shell's system chat registers the host tool on the system session over its own connection (octos#2567's host session target, peer/tools/register without peer and, since octos#2657, without any app peer's host token, so it is offered before any app's agent has started) and withdraws it when the switch goes off; the shell types each approved call into the Terminal the person sees.

What the kernel enforces. Every start writes the _main profile's tool_policy (system_tools::tool_policy): everything a grant can give, minus octos's own shell (group:runtime: shell, bash, exec_command, write_stdin), the one tool OctoSense never offers, since §12 grants command execution only as a host tool, and peer_close. octos applies it to every turn of the profile, wake continuations included: the ceiling. So:

  • The system agent gets exactly its list (§12, plan step 4): every kernel start sets the system session's kernel tools to SystemAgentTools::kernel_tools (octos session/tool_list/set, octos#2648) on the host's own connection, before any consumer's frame reaches the kernel, and system_tools::set_grants sets it again on a running kernel. octos keeps the list durably and narrows every turn on the session with it, whoever starts it (the shell, a Talk to Octos client, a wake continuation). Its host tools (command execution's terminal.run) are registered by the system chat and are not filtered by it; the spawn family is never on it (SPAWN_FAMILY). Real-kernel test: a_system_agent_turn_is_offered_exactly_the_system_agent_tools;
  • app peers are narrowed to their grants by their turns' generic_tools (plan step 6);
  • Talk to Octos external turns keep octos's own allowlist.

The policy is written only into OctoSense's own core dir, and only over a policy OctoSense wrote ("owner": "octosense"): a foreign policy, or the person's own $HOME/octos-home/.octos, is refused with a warning.

The system agent's skills

ADR 0013: the system agent learns each craft engine linked into the build from an octos skill, so it reads an engine's instructions when a request needs them instead of carrying every engine method as a tool schema on every turn. The shell registers the skills once at startup (skills::set_managed, from crates/shell/src/system_chat/skills.rs); before every kernel start, launch::prepare syncs them (skills::sync) into the skills dir octos reads for the _main profile:

  • Where. <core_dir>/profiles/_main/data/skills/<name>/: the profile's data dir, or <data_dir>/skills when _main.json names an absolute data_dir, as octos's ProfileStore::resolve_data_dir does. When the profile runtime bootstraps, octos lists each skill's name, description and SKILL.md location in the profile's system prompt (## Available Skills), so every session of _main, app agents' included, sees the summary from that start on. The dir is also a read zone of every session's file tools, which is why it has to exist before the start.
  • Managed, never the person's. Each managed skill holds .octosense-managed. A sync writes a missing or changed skill (built beside it and swapped in by rename), removes a managed skill that is no longer registered, and never writes, moves or removes a skill without the marker, even one with a registered name (that skill is then reported as not installed). Nothing happens before the shell registers a set, and nothing is written into the person's own $HOME/octos-home/.octos.
  • Fails open. A skill is guidance, not a boundary: one that cannot be installed is logged (octos-core: managed skills in <dir>: ...) and the kernel starts anyway. Only the tool policy fails closed.
  • Checked. skills::validate refuses a skill octos would misread: a name outside [a-z0-9-], a file path outside the skill's folder, a SKILL.md whose frontmatter names another skill, a description that is missing, over 200 bytes, quoted, or holds # (octos cuts there) or <, >, & (its summary is unescaped XML), and always: true (which would load the whole body into every turn).

Testing

From the repository root:

cargo test --locked -p octosense-kernel   # unit tests + the core against a stand-in kernel (python3)
# The real kernel: a profile written by octosense-llm-config, session/open,
# profile/llm/list, a provider change and a restart; Talk to Octos on and off
# (what the external token must not reach, pairing, restart and rotation);
# native and web clients on one system conversation; and a host killed with
# SIGKILL taking its kernel with it. Build octos at the rev the root
# Cargo.toml pins, then:
OCTOS_CORE_TEST_KERNEL=/path/to/octos cargo test -p octosense-kernel --test real_kernel -- --nocapture

Build the kernel for that test (and for an Android APK, with the NDK and --target aarch64-linux-android) from octos-org/octos at the rev in the root Cargo.toml [workspace.dependencies]:

cargo build --release -p octos-cli --bin octos --no-default-features --features api,git,ast

python3 tools/kernel-artifact.py --host --plan prints the same build of the pinned revision for this desktop.

CI: .github/workflows/apps.yml (the services job tests this crate; the apps job builds AppCard, which links it).

One octos

This crate links octos-core (for the stdio feature set) and, on OpenHarmony, octos-cli, from git octos-org/octos at the one rev the root Cargo.toml pins for every octos crate. A workspace that builds it for OpenHarmony also needs the nix patch (octos rev 18fcd3f1, see the root Cargo.toml [patch.crates-io]). Elsewhere the kernel is a separate binary built from that same rev.