First Version
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
# Tool Description & Push Delivery (`describe` + `blocks` + APNs)
|
||||
|
||||
_Normative for approval rendering on the client side._
|
||||
|
||||
> How the app receives and displays **what** it is approving, and how the push notification stays
|
||||
> lightweight. Primarily concerns **approvals** (tool arguments are hard to read as raw JSON);
|
||||
> clarifications already carry a human-written `question` from the LLM.
|
||||
>
|
||||
> This file defines the **wire contract** (what the client receives). How the blocks are
|
||||
> produced on the agent side (built-in vs MCP, templates) is in the agent's tool description
|
||||
> infrastructure.
|
||||
|
||||
---
|
||||
|
||||
## 1. Two Representations, One Item
|
||||
|
||||
Every `approvals[]` entry in an `inbox_update` ([payloads.md §3.1](payloads.md)) carries **two**
|
||||
views of the same tool call:
|
||||
|
||||
| Field | What it is | Used for |
|
||||
|-------|-----------|----------|
|
||||
| `summary` | Short human string, generated by `describe(Short)` on the agent (e.g. *"Send an email to mario@acme.com"*) | Card row, **push notification**, conversation log |
|
||||
| `blocks` | **Structured** parameter description, generated by `describe_view(args)` | Detail screen (forms/tables/diffs) |
|
||||
| `arguments` | Raw args (tool's JSON) | Final fallback / debug |
|
||||
|
||||
`summary` is the source of truth for "narrow" surfaces (notification, badge); `blocks` for the
|
||||
detail screen. `arguments` remain as a safety net.
|
||||
|
||||
---
|
||||
|
||||
## 2. `blocks` Schema (wire)
|
||||
|
||||
A tool call is described as a **list of typed blocks**. The vocabulary is **small and stable**:
|
||||
tools are unlimited, block types are not. Each client maps types to its native widgets *once*,
|
||||
and it works for any present and future tool.
|
||||
|
||||
```json
|
||||
{
|
||||
"v": 1,
|
||||
"summary": "Send an email to to@mail.com",
|
||||
"blocks": [
|
||||
{ "type": "key_value", "key": "Email-To", "value": "to@mail.com", "value_type": "email" },
|
||||
{ "type": "field", "label": "Subject", "value": "Q3 Estimate", "value_type": "string" },
|
||||
{ "type": "block", "label": "Body", "value": "…body…", "value_type": "text" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Example for `write_file`:
|
||||
|
||||
```json
|
||||
{
|
||||
"v": 1,
|
||||
"summary": "Write /path/x.rs",
|
||||
"blocks": [
|
||||
{ "type": "key_value", "key": "File", "value": "/path/x.rs", "value_type": "path" },
|
||||
{ "type": "block", "label": "Diff", "value": "= same\n- old line\n+ new line", "value_type": "diff" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**`type`** = layout hint (three values suffice):
|
||||
|
||||
| `type` | Rendering |
|
||||
|--------|-----------|
|
||||
| `key_value` | Compact `key: value` row |
|
||||
| `field` | Labelled field, value on one line |
|
||||
| `block` | Extended content with label (multi-line / dedicated viewer) |
|
||||
|
||||
**`value_type`** = value semantics → widget + formatting:
|
||||
|
||||
`string` · `text` · `markdown` · `code` · `diff` · `command` · `email` · `url` · `path` · `json` · `number` · `boolean` · `datetime` · `secret`
|
||||
|
||||
Rendering notes:
|
||||
- `diff` → diff viewer (green/red). `command` / `code` → monospace. `email` / `url` → tappable.
|
||||
- `secret` → **masked by default** (e.g. API key in args).
|
||||
- Block fields: `key` (for `key_value`) or `label` (for `field`/`block`), `value`, `value_type`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Delivery Model: Lightweight Push, Detail via WS
|
||||
|
||||
**Principle: the push never carries `blocks`/`arguments`. Only the `summary`.**
|
||||
|
||||
| Channel | What travels |
|
||||
|---------|-------------|
|
||||
| **WS** (live or `inbox_request` on tap) | **Complete** `inbox_update`: `summary` + `blocks` + `arguments` |
|
||||
| **APNs/FCM push** | A minimal `notification` (kind §3.2 in payloads.md) with `body` = `summary` (`describe`), **no blocks/args** |
|
||||
|
||||
Flow: the app receives the notification with the `summary` line → user taps → app opens WS and
|
||||
sends `inbox_request` ([payloads.md §4.6](payloads.md)) → receives the complete `inbox_update`
|
||||
with `blocks` → shows the detail screen.
|
||||
|
||||
### Why (zero-trust + size)
|
||||
|
||||
- The relay is **zero-trust**: it cannot read the encrypted content, so it **cannot** extract
|
||||
`summary` from the `inbox_update` blob. It is the **agent** that emits, for the push, a
|
||||
lightweight `notification` payload (E2E, decrypted by the NSE) containing only the `summary`.
|
||||
- This way the push **always** fits under the content-in-push threshold (3500B b64,
|
||||
[server.md §5](server.md)) → notification always readable, without the content-vs-wake juggling
|
||||
needed for rich payloads.
|
||||
- The `aps.alert` in plaintext ("Skald / Action required") remains the generic fallback visible
|
||||
to Apple; the real text (`summary`) is in the encrypted blob that the NSE replaces.
|
||||
|
||||
---
|
||||
|
||||
## 4. Forward-Compat & Degradation
|
||||
|
||||
- `v` at the top of `blocks`. An unknown `type`/`value_type` is not an error: the client
|
||||
degrades to `key_value` / raw string (never crash).
|
||||
- If `blocks` is absent (older agent, or tool with no description), the client shows `summary`
|
||||
and optionally raw `arguments`. Fully backward-compatible.
|
||||
- Non-form surfaces (e.g. Telegram) **flatten** blocks to text (`key: value`, diff in monospace):
|
||||
every block is linearisable by construction.
|
||||
Reference in New Issue
Block a user