# A personal streamline — `*.streamline.json`

One file, one streamline, owned by one person: the device and private parts in one flat object. No manifest, no
signature, no write API: the person imports it in the app (Files, AirDrop, or a `streamline://import?data=<base64url>`
link). Model calls go straight to the provider with the person's own key from the app's vault. **The file never
contains a key, token or sign-in**; the app refuses one that looks like it does.

## Contents
- Example
- Fields
- Tool policy
- Templates and renderers
- Checklist

## Example

```json
{
  "schema": "streamline.personal/1",
  "id": "orders_desk",
  "name": "Orders",
  "description": "Look up and update my shop's orders.",
  "icon": "shippingbox.fill",
  "color": { "light": "#1D5C8C", "dark": "#5AA9E6" },
  "instructions": "You help one person handle one order per chat. Use the tools. Changes wait for their tap. A result with ok:false is the system's answer: relay it plainly.",
  "model": { "provider": "anthropic", "id": "claude-sonnet-5-5", "effort": "medium", "maxTokens": 32000 },
  "mcp": [ { "id": "shop", "name": "Shop", "url": "https://mcp.shop.example/mcp", "auth": "oauth",
             "tools": { "readOnly": ["order_get", "*_list", "*_search"], "confirm": ["refund_*", "order_cancel"],
                        "confirmWhenArgs": ["settings"], "hidden": ["admin_*"], "direct": ["order_get"] },
             "renderers": { "order_get": "order" } } ],
  "templates": { "order": { "root": { "type": "stack", "spacing": 6, "padding": 14, "children": [
    { "type": "text", "text": "Order {{ $.number }}", "style": "headline" },
    { "type": "keyValue", "rows": [ { "label": "Total", "value": "{{ $.total | money }}" },
                                    { "label": "Status", "value": "{{ $.status | title }}" } ] } ] } } },
  "imageHosts": ["img.shop.example"],
  "welcome": "Each chat is one order. Give me an order number or a customer.",
  "placeholder": "Order number, customer, refund…",
  "quickPhrases": [ { "label": "Today's orders", "text": "Show today's orders", "send": true } ]
}
```

## Fields

| Field | Req | Notes |
|---|---|---|
| `schema` | no | `streamline.personal/1`. |
| `id` | no | `[a-z0-9_]{2,32}`; made from the name when absent. Importing an existing id replaces that streamline. |
| `name` | **yes** | |
| `description`, `icon`, `color`, `theme` | no | As in streamlines.md (`theme`: a preset id, two-sided, or `{ base?, accent? }`; an own theme inline is not built). |
| `logo` | no | `data:image/png;base64,…` (or `jpeg`), ≤ 256 KB decoded, square: inline so the file stays one file. Drawn where the icon would be; the icon stays the fallback. |
| `instructions` | no | The system prompt. No business rules as rules, no secrets. |
| `briefs` | no | Personal briefs to include first, in order (`personal/briefs/<id>.md` beside the file; briefs.md). |
| `model` | no | `provider` `anthropic`; `id` (default `claude-sonnet-5-5`; Haiku 5.5 and Opus 5.5 offered); `effort`; `maxTokens` (32000). The key is `vault:<id>` if the person set one, else `vault:default`; never write a key. |
| `mcp[]` | no | `id`, `name`, `url` (https; `http://localhost` only in debug builds), `auth` `oauth` \| `none` (built), `password` \| `api-key` (designed), `tools` (policy), `renderers`, `reauthOn`. |
| `mcp[].reauthOn` | no | Business error codes that mean "the session behind this token ended", for servers that answer them with HTTP 200 instead of 401. The app discards the token, asks the person to sign in again and retries. `oauth` only. |
| `templates` | no | `{ id: { "root": node } }`, ids `[A-Za-z0-9_.-]{1,64}`; the same language and limits (templates.md). A failing template is left out; its tools get the automatic card. |
| `imageHosts` | no | Hosts template images may load from. |
| `attachments` | no | The paperclip, the same object as an organization's (streamlines.md, *Attachments*). The files go from the device straight to the model with the person's key; `chatMaxBytes` (default 16 MB) keeps a stream within one request. |
| `scan` | no | The scan button, the same object as an organization's (streamlines.md, *Attachments and scanning*). A fast-path `action.tool` must be in a server's `tools.direct`, and not in `tools.confirm` when `modes` allows continuous or sweep scanning. |
| `archive` | no | `anytime` (the default for a personal streamline), `whenDone` or `never`. |
| `welcome` | no | Local bubbles; paragraphs split on a blank line. |
| `placeholder` | no | Composer hint. |
| `quickPhrases[]` | no | `label`, `text`, `send` (default true). The first four are also the starter chips. |
| `composer.suggestions[]` | no | Chips that follow the stream's state after the first message, the same rules as an organization's (streamlines.md, *Suggestions*). A personal file has no overview, so rules read `last.tool` / `last.result.*`, `chat.*`, `stream.*`, `user.*`, `tenant.*` (`overview.*` is `suggestions.noOverview`); a `tool` chip's tool must be in a server's `tools.direct`. Example: `{ "when": { "path": "last.tool", "eq": "invoices_list" }, "label": "Open the first", "text": "Open invoice {{ last.result.invoices[0].id }}", "send": true }`. |
| `composer.modelSuggestions` | no | `true` lets the model end each reply with up to three things the person might say next, shown as chips only when no rule holds. Off by default; about 90 tokens a turn on the person's key; the chips are words only and never reach the transcript. Personal files only. |

## Tool policy

Per server, all lists optional, `*` globs: `readOnly` (run untapped), `confirm` (always tap), `confirmWhenArgs`
(tap only when called with any argument), `hidden` (never offered, refused if called), `direct` (a card button may
call it). Precedence: hidden > confirm > confirmWhenArgs > readOnly > annotations > a tap. See tool-policy.md.

## Templates and renderers

`renderers` maps a tool to a template id in the same file. The card binds `$` to `structuredContent`, or to `data`
for an `{ok, data}` result. Draft templates from a **real** result (run the read tool once with the person signed in,
or ask for a sample with invented data), never from a guessed shape.

## Checklist

- [ ] `name` set; `id` matches `[a-z0-9_]{2,32}`
- [ ] Every server inspected; every tool settled as readOnly, confirm, confirmWhenArgs or hidden
- [ ] Writes confirm-gated; admin and bulk tools hidden
- [ ] No key, token, password or personal data anywhere in the file
- [ ] Every renderer names a template in the file; templates checked
- [ ] `https://` URLs (localhost only for a debug test)
- [ ] The checker is clean
