Files

128 lines
6.2 KiB
Markdown

# Android Plugins
Hermes Android exposes opt-in, plugin-provided pages through the authenticated
upstream Dashboard connection. The product name is **Plugins**. The implementation
borrows the contribution model from the official Hermes Desktop Plugin SDK while
using an Android-specific execution boundary.
## Security boundary
Android never downloads or evaluates plugin JavaScript, Kotlin, bytecode, WebViews,
or arbitrary Compose code. A backend plugin supplies a bounded, versioned JSON
document and the app renders it with host-owned Compose components. Documents cannot
provide network URLs, Android intents, filesystem paths, gateway calls, secrets, or
authentication material.
The Dashboard plugin catalog establishes plugin identity. All follow-up reads and
writes stay below `/api/plugins/<plugin-id>/...`; the client rejects traversal,
queries, fragments, absolute URLs, and cross-plugin paths. Enablement and grants are
stored locally per connection, effective profile, and plugin.
Read-only pages are available after the user enables a plugin. Mutating actions also
require the local `plugin.api.write` grant. Actions with a confirmation string require
an additional per-use confirmation in the app. The app never gives plugins direct
access to session tokens or cookies.
## Discovery contract
1. Read the authenticated catalog at `GET /api/dashboard/plugins`.
2. Probe catalog entries that expose an API at
`GET /api/plugins/<plugin-id>/mobile/manifest`.
3. Accept only manifests whose `id` matches the catalog identity, whose
`schema_version` is supported, and whose `min_host_api` is compatible.
4. Load enabled page contributions through their plugin-scoped document endpoint.
The v1 manifest shape is:
```json
{
"schema_version": 1,
"id": "weather",
"display_name": "Weather",
"description": "Local forecasts and alerts",
"version": "1.0.0",
"min_host_api": 1,
"default_enabled": false,
"requested_capabilities": [
{ "id": "plugin.api.write", "reason": "Save alert preferences" }
],
"contributions": [
{
"id": "forecast",
"surface": "page",
"title": "Forecast",
"document": { "method": "GET", "path": "mobile/pages/forecast" }
}
]
}
```
## Declarative page surface
Schema v1 supports complete scrollable pages composed from groups, cards, text,
badges, buttons, text inputs, toggles, progress indicators, opaque plugin-scoped
images, dividers, and spacers. Literal values and state bindings drive conditional
visibility and enabled state. Host-owned fade, scale, vertical-slide, and highlight
transitions make pages reactive and animated without allowing executable UI code.
They become immediate static updates when system animations are disabled or touch
exploration is active.
Button actions may call a relative endpoint in their own plugin namespace. Write
requests receive both the action's declared arguments and the page's current bound
state, then the host reloads the document so the plugin remains authoritative. The
host validates document size, nesting, identifiers, bindings, text lengths, animation
values, and asset references before rendering.
Future schema versions can add elements or contribution surfaces deliberately. An
unknown v1 element is rejected rather than interpreted as code or rendered in a
WebView.
## Relationship to chat cards
Existing model-generated `CARD:{json}` content remains presentation-only chat output.
It does not identify a plugin, inherit plugin grants, or gain access to plugin action
endpoints. Plugin authority always begins with the authenticated Dashboard catalog and
the matching mobile manifest.
## Live agent-created pages
When the optional Hermes-Relay plugin is installed, the active Hermes agent can use
the `relay_plugin_draft` tool to create or replace a generated declarative page. The
tool accepts the same bounded schema as Android, stores JSON atomically below
`HERMES_HOME/mobile-plugins`, and rejects every `action.request`. Generated previews
therefore cannot reach Relay management APIs or acquire executable backend behavior.
The contribution ID `git` is reserved for the Relay plugin's native Git workspace;
generated drafts cannot shadow or duplicate that route.
Android presents that reserved contribution as a first-class native surface rather
than a generic declarative page. When the live plugin API confirms Git is available,
Chat can show a compact branch/change rail and a dot-only context action; **Chat →
Show Git workspace in Chat** hides those two Chat affordances without disabling the
workspace. The full native workspace remains reachable from Settings and Plugins and
owns repository/branch selection, diffs, staging, confirmed destructive actions,
commits, and remotes. Session `git_repo_root`/`cwd` metadata selects an exact matching
repository when possible; ambiguous catalogs require an explicit user choice. Mobile
discovery alone is not treated as runtime readiness: a missing Git API route renders a
retryable unavailable state instead of exposing the raw Dashboard error.
The Relay mobile manifest exposes drafts as preview pages under the authenticated
`hermes-relay` plugin namespace. Android polls the catalog every five seconds while
the Plugins hub is visible and polls a visible generated page every five seconds.
No catalog or page polling continues after that surface leaves the foreground.
Publishing and persistent removal are user decisions:
1. The agent creates or updates a draft and may list it.
2. Android displays it as **Draft preview**.
3. The user enables the Relay Plugins surface and its scoped write grant.
4. **Keep** publishes the exact current draft; **Remove** requires confirmation.
5. The agent-side publish/remove tools return `approval_required` instead of applying
persistent mutations themselves. Only a session-lifecycle draft may be removed
directly by the agent because that operation is local and reversible by redrafting.
The first store version is host-local and uses lifecycle labels, but does not isolate
draft visibility between simultaneous Hermes sessions. True task-private previews,
profile/connection publication precedence, revision-bound approvals, and push catalog
invalidation remain future protocol work. The current polling path is deliberately
small and requires no upstream fork.