Skip to content

ADR 0004 §8/§4: approval router, standing rules, shell sheets, first-use consent - #120

Merged
ymote merged 2 commits into
mainfrom
adr0004/approvals
Sep 28, 2026
Merged

ymote merged 2 commits into
mainfrom
adr0004/approvals

Conversation

@ymote

@ymote ymote commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

ADR 0004 plan step 7 (§8 approvals, §4 consent): the shell's approval surface. The new code lives in crates/shell/src/approvals/. It is wired into #118's developer mode, the AI bus's confirm: host path, the module host's offer and the contained apps' octos service.

What's built

The router (router.rs) takes each request on its exact arguments and checks, in order:

  1. developer mode (dev_hooks.rs: answers_approval, overrides_app_confirm, approves_command, grants_all). It approves auto_approvable: false too, but never for an external client;
  2. confirm: app tools: handed to the owning app's registered sheet (AppConfirm), with the caller and context. If the app isn't running, the request waits app_wait_s (120 s), then is refused visibly;
  3. auto_approvable: false, outcome_unknown and external calls always go to the person;
  4. standing rules. Runs started by incoming content (or with an unknown trigger) are skipped unless a rule opts in;
  5. otherwise a sheet: one per request, or one batched sheet in the system chat per Batch. An unanswered sheet line expires and is declined.

Every decision goes to the relay once and to the audit. Every automatic one is also a notification.

Relay seam (relay.rs):

  • in: ApprovalIntake::approval_requested(app, tool: ToolSpec, args, caller, context);
  • out: ApprovalRelay::approval_decided(id, Decision::{ApproveOnce | Deny | ApproveByRule(rule_id)}, reason);
  • approvals::set_relay installs the relay and hands over decisions made before it; RecordingRelay is the test double.

Standing rules (rules.rs):

  • keyed on (owning app, tool), whoever calls;
  • conditions: recipients in contacts, in the thread, or either; no attachments; triggered by the person; amount and count limits;
  • daily cap per UTC day, persisted, so it survives a restart;
  • a time-boxed "everything this app asks", 60 min at most (no forever rule), with a top-of-screen indicator and a Stop button;
  • one-tap all off;
  • created only with a PersonGesture, which only the sheet and the Settings page construct (a test scans the sources);
  • stored per home in <home>/approvals/rules.json, 0600.

Audit (audit.rs): <home>/logs/approvals-audit.jsonl, append-only and 0600. Each entry has the rule, app, tool, a SHA-256 digest of the arguments (never the arguments), caller, trigger, who answered, time and result.

Sheets (sheet.rs model, view.rs view): drawn in the shell's chrome kit, like the menu, notifications and dev banner. Each line shows the owning app and tool, "Asked by …", the pretty-printed arguments with secrets redacted (by schema secret_fields and by name), and why it needs the person. Answers per line: Approve once, Deny, or up to two "Always …" choices (contacts, this thread, when I start it, 1 hour, everything for 60 min). Choosing one creates the rule and answers the other open lines that now match. An agent's text is never an approval surface.

Settings → Assistant → Approvals (settings_page.rs):

  • rules with cap use and time left, each with Delete (or Turn on);
  • Turn every rule off;
  • every app's agent with its consent, an off switch and "Everything for 60 min";
  • the recent automatic approvals.

First-use consent (consent.rs): the first time an app asks, a sheet shows what the agent may read and use (from the manifest and grants) and where the model runs. Allow or Don't allow is remembered in <home>/approvals/consent.json. approvals::consent::granted(app) is there for #106's contained apps and the Rinx/native offer path. Developer mode skips the prompt and stores nothing.

Tests

  • cargo test -p octosense-shell --lib: 432 pass (443 with mobile-apps), 35 of them in approvals, plus the reworked ai_bus Terminal test (held, released, refused, dropped on death) and a menu test. octosense-ai-host tests also pass. The approvals tests cover:
    • precedence: dev mode beats auto_approvable: false and confirm: app; never for an external client;
    • every rule condition, the cap and next-day reset, time-box expiry, the 60-min limit and no-forever;
    • the incoming-content exclusion and opt-in, narrow-before-broad, all off;
    • the sheet model (redaction, labels, batching, "always" creating a rule), sheet expiry;
    • the confirm: app hand-off, wait, timeout and unregister;
    • persistence and 0600 modes, append-only, consent;
    • the gesture source scan, the relay queue, bus decisions and the module consent gate.
  • Hidden-window desktop run (MAKEPAD_HIDE_WINDOWS=1 MAKEPAD_REMOTE, throwaway OCTOSENSE_HOME, ended with /gq):
    • --test-action approval-sheet: the sheet drew, with smtp_password redacted. Approve once closed it and wrote a person/approved audit line (0600);
    • consent sheet, then Allow; the sheet's "Always when I start it" created r1;
    • on the next launch the same call was auto-approved by r1. The Settings page listed the rule ("2 of 20 today"), News's agent (Allowed) and the log line;
    • "Everything for 60 min" showed the indicator, and Stop removed it;
    • after the rebase, the batched system-chat sheet (2 lines): one approved, one denied, both audited.
  • cargo check desktop and phone (mobile-apps), tools/check-shell-graph.sh for both, and tools/setup.py --check --cargo all pass.

Wired in (after #118)

  • Developer mode is asked first. dev_hooks::ShellDevMode forwards to:
    • dev_mode::answers_approval (as HostConfirm), overrides_app_confirm, approves_command and grants_all;
    • dev_mode::audit_auto_approval, which writes each approval it gives to developer mode's own audit.
  • AI bus confirm: host path (host_confirmed / apply_rules, e.g. the Terminal's run): those calls now go through the router, with nothing duplicated.
    • The tool is no longer marked destructive for the pane's card.
    • Each call is held (Route::Approval) until the router answers, then AiBus::release sends it on or refuses it to the pane.
    • run is a command and auto_approvable: false, so developer mode approves it through approves_command and every other run asks the person on the sheet.
    • The card's 60-byte argument limit is gone, because the sheet shows the arguments in full.
  • Consent at first use:
    • it gates the assistant offer in module_host: the first time, the sheet asks and that instance goes without; once allowed, the next instance gets its agent;
    • it gates the contained apps' octos service (ai_host::contained::set_consent, installed by approvals::init);
    • storage offers are not gated: they are the app's own data, not its agent's.
  • Settings entry: Setup → Assistant → Approvals opens the page. There is a menu test; the row was not clicked in the hidden run, because typing into the hidden window's launcher did not register.

Remaining

  • The octos#2567 relay (home-96) plugs in through approval_requested / set_relay. The app-peers broker's octos approvals get to the router once that relay exists.
  • Rinx's send sheet doesn't register as an AppConfirm yet (the module contract can't carry a host confirmation).
  • The in-process pane leg (pane_links.rs) applies no host rules, before or after this PR.
  • There is no contacts source yet (NoContacts). Rule fact fields use conventional argument names until tools.json fields are agreed.
  • Phone: not run on a device (unverified).

🤖 Generated with Claude Code

ymote and others added 2 commits September 28, 2026 15:36
…nt [skip ci]

ADR 0004 §8/§4 (plan step 7), in new modules under crates/shell/src/approvals/:

- router: developer mode first (adapter, stubbed until #118), then the
  confirm: app hand-off to the owning app's sheet (wait with a timeout or
  refuse visibly), auto_approvable:false / outcome_unknown / external calls
  always to the person, standing rules (incoming content excluded by
  default), else a sheet; every decision audited, every automatic one
  notified
- rules keyed on (owning app, tool) with contacts / thread / attachments /
  person-triggered / amount / count conditions, a daily cap, a time-boxed
  "everything" (max 60 min) with an indicator, one-tap all off; created only
  with a PersonGesture from Settings or a sheet; persisted per home
- sheet model and shell-drawn sheets (one per request, batched in the
  system chat), args pretty-printed with secrets redacted, approve once /
  deny / always for ...
- Settings > Assistant > Approvals page; first-use consent with
  consent::granted(app)
- relay seam: ApprovalIntake::approval_requested in, ApprovalRelay::
  approval_decided out, with a recording test double
- append-only owner-only audit (args digest only)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…onsent [skip ci]

- dev_hooks: ShellDevMode forwards to dev_mode (answers_approval as
  HostConfirm, overrides_app_confirm, approves_command, grants_all) and
  writes developer mode's audit for each automatic approval it gives
- ai_bus: confirm:host tools (the Terminal's run) are no longer parked on
  the pane's card; each call is held (Route::Approval) until the router
  answers, then released to the app or refused to the pane. The card's
  argument-length refusal is gone: the shell's sheet shows the arguments
  in full
- consent at first use gates the assistant offer in module_host and the
  contained apps' octos service (ai_host::contained::set_consent)
- Setup > Assistant > Approvals opens the Approvals page
- the gesture is ApprovalGesture, so dev_mode's setter scan stays exact

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ymote
ymote merged commit 6e0d475 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>
ymote added a commit that referenced this pull request Sep 29, 2026
ADR 0004 §5. peer_link/ gives a process app its own channel to its octos
app agent on its hub socket ("octos_peer" frames, never the AI bus):
requests on its one peer through the app-peers service in-process
modules get (an adapter until octos#2567's relay), and the tool-call
half behind seams for the relay (set_tool_relay, tool_call, tool_cancel,
context_owner, has_link, close_account).

Identity is the socket's slot; a call's account, context and client come
from the shell's own context records. Links open only for apps whose
native-apps.json entry grants agent.octos, and requests need consent.
The shell keeps the host obligations (once per call, nothing after
cancel, acknowledge before any sheet; confirmations through the #120
router, whose decisions get their own peerlink: route), and a dead
process fails its calls (outcome_unknown unless read-only), closes its
contexts and keeps the peer.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ymote added a commit that referenced this pull request Sep 29, 2026
ADR 0004 §5. peer_link/ gives a process app its own channel to its octos
app agent on its hub socket ("octos_peer" frames, never the AI bus):
requests on its one peer through the app-peers service in-process
modules get (an adapter until octos#2567's relay), and the tool-call
half behind seams for the relay (set_tool_relay, tool_call, tool_cancel,
context_owner, has_link, close_account).

Identity is the socket's slot; a call's account, context and client come
from the shell's own context records. Links open only for apps whose
native-apps.json entry grants agent.octos, and requests need consent.
The shell keeps the host obligations (once per call, nothing after
cancel, acknowledge before any sheet; confirmations through the #120
router, whose decisions get their own peerlink: route), and a dead
process fails its calls (outcome_unknown unless read-only), closes its
contexts and keeps the peer.

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
…DR 0004 §12)

Re-port of OctoSense-org#108 onto main's octos#2567 shell side (OctoSense-org#145), behind the
`toolbox-peers` feature (off by default):

- the toolbox is one more owning app (`toolbox`) in the shell's host-tool
  relay: its tools are declared once in the catalog (`app: "toolbox"`,
  shareable), each app is granted exactly what it declares AND the person
  granted, and `ToolboxExecutor` (crates/ai-host) runs the calls the relay
  routes to it; main's broker registers them after every prepare and
  reconnect. OctoSense-org#108's own registration and routing are not ported;
- no hard-coded exclusions: OctoSense-org#108's "never offer octos deep_research"
  (its empty generic_tools) is gone;
- the OctoSense-org#120 first-use consent gates the toolbox: nothing offered
  (Catalog::offered) and nothing run (the relay's consent_pending) before
  the person allowed the app's agent;
- ported from OctoSense-org#108: crates/toolbox `peer` (plus a shared `catalog`),
  read_links/narrow_crawl, ModelHostClient, toolbox_folder, the 256 KiB host
  model input, the mock model's CALL_TOOL rule; tests against a scripted
  kernel through main's broker, the relay, and a real octos kernel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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