Skip to content

Terminal as a system app and native-apps.json: one manifest, hosting per target (ADR 0004 steps 1–2, §10) - #113

Merged
ymote merged 9 commits into
mainfrom
adr0004/manifest
Sep 28, 2026
Merged

ymote merged 9 commits into
mainfrom
adr0004/manifest

Conversation

@ymote

@ymote ymote commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

ADR 0004 plan steps 1–2, including the former #112 (Terminal as a desktop system app), folded in at the reviewer's request so Terminal never lands in-process-only with a read-only AI.

From #112 (three commits at the base):

  • app-terminal feature (in the desktop default set) links makepad-terminal's TERMINAL_MODULE; on macOS the PTY helper is octosense itself (--exec-pty), so nothing ships beside the binary for the in-process form.

  • Keyboard focus fixes for module tiles: a pending focus is re-asserted after the tile draws, and the tile that last took the keyboard releases it on a focus change.

  • Phone builds unchanged (mobile-apps does not include Terminal).

  • native-apps.json declares every native app once. For each app it gives:

    • the crate, source pin, module and bin;
    • hosting per target, and which shells include it;
    • sandbox, storage and agent.
  • tools/native_apps.py generates what used to take five hand edits, as marked # BEGIN native-apps blocks:

    • the root Cargo.toml pins and [patch] entries;
    • crates/shell/Cargo.toml optional deps, app-<id> features and mobile-apps;
    • the desktop/ and phone/ defaults;
    • all of crates/shell/src/native_apps.rs (what linked_modules() links, and the hosting table).
  • --check fails CI on drift (desktop.yml, phone.yml). It refuses process on mobile/wasm, process without a bin, and two revisions of one repository. Cargo.lock changes only through cargo.

  • The native News, Maps and Photos crates are deleted; the script apps are the only versions. The Photos sample library moves to apps/photos/resources/, where Home mounts it.

  • Hosting comes from the manifest. Terminal is a process on macOS and Windows (on Linux only with Vulkan and Wayland). If no checkout or sibling binary exists, it falls back to in-process, so packaged builds keep the in-process Terminal until Desktop release packages (macOS .app/.dmg, Windows NSIS, Linux .deb/.AppImage) and release-desktop workflow #94 ships the bin. App Hub, Rinx and AppCard are in-process, so Rinx and AppCard now open on the desktop without --module.

  • Process launch fixes:

    • process apps launch with cargo run --locked, so a checkout's lock is never rewritten;
    • a new Terminal opens in ~ (via --cwd);
    • the Terminal's AI reads and types commands in both hostings (ADR §10): run stays registered, marked confirm: host, auto_approvable: false in native-apps.json (agent.tool_policy); the shell registers it as destructive so every call waits for the live confirm card, refuses a command the card cannot show in full, and runs its own full executor for the in-process Terminal so both hostings offer the same tools.

The generated TOML parses identically to the old hand edits. Cargo.lock differs from main only by the four deleted packages.

Verification

  • cargo build --locked -p octosense
  • the graph checks (desktop and phone)
  • cargo test -p octosense-shell --features app-terminal: 348 passed
  • phone with mobile-apps: 358 passed
  • native_apps.py --check
  • Python tests: 44 passed
  • a hidden desktop run: Terminal launched as a process, ran zsh, and pwd printed $HOME; ended with /quit

Not done

  • Reference and Sheets stay process on Linux (unchanged).
  • Agent grants are declared and checked against Policy::shipped() by a test, but not generated yet.

🤖 Generated with Claude Code

@ymote
ymote force-pushed the feat/terminal-system-app branch from e7ef0ca to 5b458a8 Compare September 28, 2026 21:37
@ymote ymote changed the title native-apps.json: one manifest for native apps; Terminal as a process (ADR 0004 steps 1–2) Terminal as a system app and native-apps.json: one manifest, hosting per target (ADR 0004 steps 1–2, §10) Sep 28, 2026
@ymote
ymote changed the base branch from feat/terminal-system-app to main September 28, 2026 21:38
ymote and others added 9 commits September 28, 2026 14:44
… draws

A focus that cannot land at launch (the tile is not drawn yet) is kept in
`pending_focus` and re-asserted when the client's first frame arrives, on the
PresentableDraw path. A module tile never sends a frame, so its pending
focus was never retried: ModuleView stayed unfocused, its gate dropped every
key, and a module opened from the dock or launcher took no typing until the
person clicked into it.

After the UI handles a Draw, retry a pending focus that belongs to a module
client. On success it clears; otherwise nothing forces a redraw, so a tile
that still cannot take it just tries again on the next natural draw.

Found with the Terminal module: typed keys never reached its root (a probe
in its key path logged nothing) until the tile was clicked; with this change
`echo` typed right after opening it from the dock ran.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Terminal was a developer program: built on first launch from the Makepad
checkout, then run as a child process. It now links in-process like Rinx:
the new `app-terminal` feature (on in the desktop's default set) registers
makepad-terminal's TERMINAL_MODULE, and `terminal` joins robrix and finance
as a module hosted in-process by default unless the person switched it
(`terminal: Process` in wm/apps.splash keeps the process form, whose
config/apps.json row stays).

- Opens instantly: no compile on first launch, no swapchain round trip.
- On macOS its PTY helper is `octosense` itself (Cx::pre_start answers
  `--exec-pty`), so nothing ships beside the binary.
- It declares `process` and `clipboard` and no `octos.*` service. The
  assistant gets read_screen and read_scrollback only; `run` (an unsandboxed
  shell, rated Risk::Act) is not offered by a default system app.
- Phone builds are unchanged: `mobile-apps` does not include it.

Builds on the current makepad pin (75e3e24d), which already carries
TERMINAL_MODULE, the PTY-helper fallback and the macOS signal-wake fix
(OctoSense-org/makepad #40, #41); Cargo.toml pins makepad-terminal there.
Tabs, panes, settings and terminal-ctl (makepad #47) arrive with a later
pin bump.

Verified locally with those makepad commits applied under .sources/makepad:
- cargo check --locked -p octosense (and --features mobile-apps)
- bash tools/check-shell-graph.sh -p octosense
- cargo test --locked -p octosense-shell --features app-terminal: 333 passed
- from phone/: cargo test --locked --features mobile-apps -p octosense-shell:
  342 passed
- hidden desktop over MAKEPAD_REMOTE: Terminal opens from the dock as
  "in-process, terminal"; the shell's parent is octosense; octoscode's
  composer stayed clean on 3 of 3 launches; closing the tile ended zsh and
  octoscode and freed the isolate; two Terminals ran two shells.
Rebased onto main: cargo build --locked -p octosense and
tools/check-shell-graph.sh pass against the pinned makepad with the reviewed
runtime patches; cargo test -p octosense-shell --features app-terminal: 345 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…us change

Focusing a process tile moves key focus to it (its forwarding needs that),
but a module tile deliberately never moves key focus: its own widgets claim
it. Nothing told the previously focused tile to let go, so after switching
between the tabs of a window group the hidden tab kept the keyboard and
every key typed into the visible one was dropped.

Track the client whose tile last took the keyboard (`keyboard_holder`); in
`focus_client`, release that tile (TileHost::release_keyboard) before
focusing another. The layout already reports the new client as focused by
then, so the previous one is tracked here rather than read from it.

Needs the matching makepad-terminal change (claim the keyboard on a key that
arrives while nobody holds it, OctoSense-org/makepad#40).

Verified: Omarchy style, a two-tab Terminal group; after Cmd+Alt+Tab text
typed runs in the tab now shown, both directions. cargo test --locked
-p octosense-shell --features app-terminal: 333 passed; from phone/:
--features mobile-apps: 342 passed; cargo check --locked -p octosense.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ADR 0004 §1 (#110): the script apps are the only versions. The native
crates were kept "for comparison" behind `app-news`, `app-photos` and
`app-maps`; nothing shipped them.

- apps/{news,maps,photos}/native, their workspace members and pins, the
  shell's optional deps, `app-*` features and place in `mobile-apps`, the
  desktop and phone forwards, and their linked_modules() lines. The
  makepad-map-nav pin and patch go with Maps (nothing else used it).
- Photos' sample library, which Home mounts into the script app, moves to
  apps/photos/resources/ with its provenance (SOURCES.md,
  generated-scenes.json); phone/system-apps.json points there. The native
  crate's catalog and icons go.
- The phone catalogs' native News/Maps/Photos rows, phone.yml's builds and
  tests of the three crates, phone/docs/{photos,maps}.md, and the README
  and ADR mentions (both languages).

Cargo.lock updated by cargo metadata (four packages removed).

Verified: cargo check --locked -p octosense; from phone/: cargo check
--locked -p octosense-home --features mobile-apps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ADR 0004 §1. Adding a native app took five hand edits that nothing
checked (the root pin, the shell's optional dependency and `app-<id>`
feature, the package defaults, a #[cfg] line in linked_modules(), and
Cargo.lock). native-apps.json at the repository root now declares every
native app the shell links: Rinx, Reference, Sheets, Terminal, AppCard and
App Hub, each with its crate and source {git, rev, local} or {path}, its
module path(s), bin, crate features, hosting per target, the shells that
link it, whether native mobile links it without flags, and its sandbox,
storage and agent grants.

tools/native_apps.py (stdlib only) owns what follows from it, between
`# BEGIN native-apps: <block>` / `# END` markers:
- Cargo.toml: the app crates' pins, and the [patch] entries that resolve
  a git source to its `local` checkout;
- crates/shell/Cargo.toml: the optional deps, the native-mobile deps, the
  `app-<id>` features and `mobile-apps`;
- desktop/ and phone/Cargo.toml: `default`, `mobile-apps` and the forwards
  (turning on the package's own optional dep where it has one);
- crates/shell/src/native_apps.rs, whole: `link()`, which linked_modules()
  now calls, and the per-target hosting table the shell will read.
`--check` exits 1 on drift. It refuses `process` on android, ios, ohos or
wasm, `process` without a `bin`, and a second revision of a repository an
app comes from. A write updates Cargo.lock only through cargo (`cargo
update -p` for a changed pin, then `cargo metadata`).

Behaviour-neutral: the generated TOML equals the previous hand edits
(compared as parsed TOML), Cargo.lock is unchanged, and link order is the
same. Hosting is unchanged in this commit. A shell test checks every
linked native module is a manifest entry and that Policy::shipped() grants
exactly the manifest's `agent.octos` (Rinx's four octos.* services).

CI: desktop.yml and phone.yml run `native_apps.py --check` (and trigger
on native-apps.json); desktop.yml's unittest discovery runs
tools/test_native_apps.py. AGENTS.md and the READMEs point at the manifest.

Verified: python3 tools/native_apps.py --check; python3 -m unittest
tools/test_native_apps.py: 16 passed; cargo check --locked -p
octosense-shell --features app-terminal --tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… process on macOS and Windows

ADR 0004 §2 and §10 (#110). A linked native app the person has not
switched is now hosted as its manifest entry declares for this target
(native_apps.rs, `NativeApp::on_this_target`), replacing the hard-coded
"robrix | finance | terminal are modules" rule (robrix and finance are not
linked by any build):

- Terminal: its own process on macOS and Windows; on Linux only with a
  Vulkan build (`MAKEPAD=vulkan`, new `makepad_vulkan` cfg in the shell's
  build.rs) in a Wayland session; in-process on phones. A crash in it no
  longer takes the shell down.
- A declared process falls back to in-process where it has no process
  form (`apps::process_form`: its launcher row resolves neither to a
  checkout to `cargo run` from nor to a sibling binary).
- App Hub, Rinx and AppCard are modules everywhere; Reference and Sheets
  keep their process default on the desktop.
- `terminal: Module|Process` (and any `<id>: ...`) in wm/apps.splash and
  `--module <id>` still win.

The process is found the way every catalog app is (clients.rs): out of a
checkout, `cargo run --release --manifest-path .sources/makepad/Cargo.toml
-p makepad-terminal --bin terminal --target-dir <home>/build/makepad`;
otherwise the `terminal` binary beside `octosense`. Release packages do
not ship that binary yet: that depends on OctoSense #94; until then a
packaged build falls back to the in-process Terminal.

The standalone terminal binary publishes `run` on the AI services bus.
The bus now withholds it (ai_bus::WITHHELD_TOOLS, keyed by the app id the
shell launched the client as): stripped from the registration and replay,
and a call to it is dropped, so the assistant gets read_screen and
read_scrollback only in either hosting.

Behaviour changes: Terminal opens as a process on macOS/Windows; Rinx
(default) and AppCard (opt-in) now open in-process without `--module`
(they have no process form, so before they were not launchable on the
desktop without the flag).

Verified on macOS: cargo build --locked -p octosense;
tools/check-shell-graph.sh -p octosense and -p octosense-home;
cargo test --locked -p octosense-shell --features app-terminal: 348
passed; from phone/: cargo check --locked -p octosense-home --features
mobile-apps, cargo test --locked --features mobile-apps -p
octosense-shell: 358 passed; native_apps.py --check; tools unittests.
Hidden desktop (MAKEPAD_HIDE_WINDOWS, MAKEPAD_REMOTE, scratch
OCTOSENSE_HOME, MAKEPAD_WM_TEST_APP=terminal): "launched terminal as
client 1" (a process, not "in-process"), cargo built the `terminal` bin
into the scratch home, first frame drawn in the tile, zsh running under
it, typed text reached it; /quit ended both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… home directory

- launch_argv passes `--locked` to `cargo run`, so launching a process app
  from a checkout never rewrites that checkout's Cargo.lock (the pinned
  Makepad under .sources/ is shared and stays as prepared); a stale lock
  now fails the launch visibly. The current .sources/makepad lock
  satisfies it.
- A Terminal with no focused terminal's directory to inherit (and no demo
  home) opens in the person's home (HOME; USERPROFILE on Windows) through
  the terminal's own `--cwd`, instead of wherever the launch ran: the
  process Terminal used to start its shell in the catalog row's checkout,
  .sources/makepad. Warm instances get the same directory.

Verified: cargo test --locked -p octosense-shell --features app-terminal:
348 passed; from phone/ --features mobile-apps: 358 passed; cargo build
--locked -p octosense; check-shell-graph.sh for both packages;
native_apps.py --check; tools unittests. Hidden desktop, scratch home:
the Terminal process ran `terminal --stdin-loop --cwd $HOME`, `pwd`
printed /Users/ychen, text rendered normally, .sources/makepad/Cargo.lock
unchanged; /quit ended both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… confirmation

Accepted ADR 0004 §10: the Terminal's AI may read and type commands in
every hosting; each command goes through the shell with a live approval
showing the exact command, `auto_approvable: false`. This replaces the
earlier stripping of the process Terminal's `run`.

- native-apps.json: `agent.tool_policy` ({tool: {confirm: host|app,
  auto_approvable}}, §8); the Terminal's `run` is `confirm: host`,
  `auto_approvable: false`. The generator validates it and emits
  native_apps::{Confirm, ToolPolicy} and NativeApp::tool().
- ai_bus: for a `confirm: host` tool (process clients by the app id the
  WM launched them as, in-process modules by their id) the registration
  keeps the tool, forced to Risk::Destructive with no self-confirm claim,
  so the pane parks every call on its live confirm card; nothing on the
  bus approves for the person (there are no standing rules, and
  auto_approvable: false keeps it so). The card shows the arguments cut
  at 60 bytes, so a call whose arguments it cannot show in full is
  refused back to the pane and never reaches the terminal.
- module_host: the in-process Terminal's module answers the reads only;
  the shell now runs its own executor for it (makepad-terminal's full
  manifest and ai::answer on the live MpTerm), so both hostings offer
  the same tools under the same confirmation.
- desktop README (both languages).

Verified: cargo test --locked -p octosense-shell --features app-terminal:
349 passed; from phone/ --features mobile-apps: 358 passed; cargo build
--locked -p octosense; check-shell-graph.sh for both packages;
native_apps.py --check; tools unittests: 46 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ral [skip ci]

The new-app test hard-coded Makepad 75e3e24d and broke when main moved the
pin to f08eec53. It now reads the revision from the Sheets entry it copies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ymote
ymote merged commit 3e7f522 into main Sep 28, 2026
ymote added a commit that referenced this pull request Sep 29, 2026
… [skip ci]

- approvals/contacts.rs: ContactsSource (is_known for email addresses and
  Matrix IDs), ContactList/NoContacts test doubles, MailContacts reading
  Mail's host service data, and ContactsGate: off by default, on only by
  the person's Settings tap ("Use my contacts in approval rules"), kept
  owner-only in approvals/contacts.json. The router evaluates "recipients
  in contacts" through the gate.
- mail host service: contacts::known_addresses (own accounts, recorded
  send recipients, To/Cc of sent folders; never the secret store); the
  decoder keeps To/Cc; a removed account forgets its recipients.
- docs/ai-services(.zh-CN).md refreshed against main (#106, #113, #117,
  #118, #120, #124, #95, #86), linking architecture.md; architecture's
  contacts lines updated.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
chrislearn pushed a commit to chrislearn/OctoSense that referenced this pull request Sep 29, 2026
The README layout tables (root, apps/, phone's `mobile-apps` row) still
listed the native comparison modules (`apps/*/native`) that OctoSense-org#113 deleted;
the script apps are the only versions. ADR 0004's Context "Today" row
now says the crates were deleted by OctoSense-org#113 (not OctoSense-org#110, as a few pages had
it). The desktop README drops the now-redundant `--module reference
--module sheets` example. Docs only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
chrislearn pushed a commit to chrislearn/OctoSense that referenced this pull request Oct 8, 2026
HUB-03's fix (930f1b2) had no test. The binary-name lookup moves into
find_app_in, which takes the rows, and
the_card_binary_names_no_card_runner_app checks that "card" names no Card
runner app while another binary still resolves; it fails with the
bin != "card" guard removed.

CAL-01 is obsolete: first-party apps are script apps (home ADR 0004), a
native app needs a reviewed native-apps.json entry (ADR 0004), and the
native Calendar source is gone from OctoScript-App-Design-Flow main.

RUNTIME-01 is obsolete as written: Maps' native crate and its isolate
tests went in OctoSense-org#113, and no test or workflow here calls init_cx_os() or
skips for it. The entry records that AppleGameInput::init is unchanged
at makepad a1c81312.

HUB-05 keeps its two open points (is_linked only used by its test; dead
android conditions in mobile_surface.rs) with current paths; the
launch_module_as log and the duplicate card filter are done.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant