# Sealed forms, hosted pages and sealed views — `forms/<id>.json`

Law 4: sensitive data never touches the model. Choose the path by what the data is:

| Data | Use | Why |
|---|---|---|
| Ordinary data (name, phone, email, address, SKU, amount) | **Ask in the stream**; it is a tool argument | The model collects and cleans it. No form. |
| Personal secrets that are not payment credentials (SSN, date of birth, license number, income, PIN) | **Sealed form** (`kind: "form"`) | Values go device → the organization's endpoint; the model gets a filtered receipt. |
| Card number, card not present | **Hosted page** (`kind: "hosted"`): the processor's own page in a browser the app can't read | Keeps card data out of the app and the model. |
| Card present at a counter | A **terminal tool** (sends the amount to the store's terminal) | Not a form. |
| Showing a secret (full account number, limit) | **Sealed view** (`kind: "view"`) | The model is told only that it was shown. |

**Card field types (`cardNumber`, `expiry`, `cvc`) exist only for `mode: "demo"` accounts.** Refuse to add them to
an organization or personal account; offer the hosted page or the terminal instead.

## Contents
- Sealed form
- Hosted page
- Sealed view
- Fields
- How the tool opens it

## Sealed form

```json
{
  "id": "financing.apply",
  "kind": "form",
  "title": "Financing application",
  "subtitle": "Goes straight to the credit partner. The assistant only learns the decision.",
  "presentation": "sheet",
  "fields": [
    { "key": "ssn", "label": "Social Security number", "type": "ssn", "required": true },
    { "key": "dob", "label": "Date of birth", "type": "date", "required": true },
    { "key": "income", "label": "Annual income", "type": "money", "required": true },
    { "key": "consent", "label": "Customer agrees to a credit check", "type": "toggle", "required": true }
  ],
  "submit": { "url": "https://credit.meridian.example/sealed/financing", "auth": "oauth", "resource": "https://api.meridian.example/mcp",
              "label": "Submit application", "include": ["session"] },
  "receipt": { "keep": { "status": "^(approved|declined|review|error)$", "ref": "^fin_[A-Za-z0-9]{6,40}$",
                         "limit": "^[0-9]{1,7}$", "message": "^.{0,140}$" } },
  "card": { "pending": "Financing application open", "done": "Financing {{ status }}{{ limit | money | prefix:' · limit ' }}" }
}
```

- `submit.url`: the organization's sensitive-data endpoint, `https`. Auth is the person's credential for the
  connection of the MCP server that opened the form. `include: ["session"]` echoes the tool's one-time session id.
- `receipt.keep`: **the only keys that ever reach the model**, each kept only if its value matches the regex in
  full. Keep it to status, a reference, last four digits, a short message. Anchor every regex (`^…$`).
- `card.pending` / `card.done`: the chat card's text before and after (`done` interpolates the filtered receipt).
- `presentation`: `sheet` (a card that opens a modal) or `inline` (≤ 3 short fields drawn in the card).

## Hosted page

```json
{ "id": "card.hosted", "kind": "hosted", "title": "Card for {{ context.customerName }}",
  "subtitle": "Opens the payment processor's secure page. Neither this app nor the assistant sees the card.",
  "allowedHosts": ["pay.processor.example"], "button": "Open secure card page",
  "receipt": { "keep": { "status": "^(captured|declined|canceled|error)$", "ref": "^tok_[A-Za-z0-9]{6,40}$", "last4": "^[0-9]{4}$" } },
  "card": { "pending": "Card entry open", "done": "Card ···· {{ last4 }} · {{ status }}" } }
```

The URL comes from the tool at run time (one-time); `allowedHosts` is the only place it may point. When the page
closes, the loop asks the tool for the outcome.

## Sealed view

```json
{ "id": "financing.account", "kind": "view", "title": "Financing account",
  "fetch": { "url": "https://credit.meridian.example/sealed/account", "auth": "oauth", "resource": "https://api.meridian.example/mcp", "include": ["session"] },
  "template": "financing.account", "expiresAfterSeconds": 120, "allowCopy": false }
```

## Fields

`key`, `label`, `type`, `required`, optional `value` (prefill, interpolated from `context.*`), `readOnly`,
`validate` (`luhn` or a regex), `options` (choice), `camera` (image), `autocapitalize`.
Types: `text`, `number`, `money`, `date`, `choice`, `toggle`, `phone`, `email`, `postalCode`, `ssn`, `image`,
`signature` (and `cardNumber`, `expiry`, `cvc` for demo accounts only). Sensitive types are masked, never
autocorrected, kept out of keyboard learning and pasteboard history.

## How the tool opens it

The MCP server returns either standard MCP elicitation (form mode with `_meta["com.manolab.streamline/ui"].sealedForm`
naming the form; URL mode for a hosted page) or the `sealedForm` directive
`{ "form": "financing.apply", "session": "…", "context": { "customerName": "…" }, "expiresAt": "…" }`. `context`
holds only non-sensitive display values. The streamline must list the form id in `forms[]` (views in `views[]`).
Nothing typed is written to disk, logged, put in a transcript or kept after the sheet closes.
