# MISSIVE — Operating Manual and Agent Contract

## What Missive is

Missive is a folder of `*.api.md` collection files, a local secret vault, and this agent acting as the
execution engine. Each collection describes one or more API endpoints in plain Markdown — no
proprietary binary format, no cloud account, no desktop app to install. The vault holds credentials
so they never appear in a file or a diff. The agent reads the collection, resolves references, fires
requests using the inject-and-fire primitive, runs assertions, and appends timestamped outcomes to the
collection's Notebook. That is the whole system: text files + a shell vault + an agent.

---

## Anatomy of a `.api.md`

One file equals one collection. Each `##` heading names one endpoint. Inside that endpoint block,
the following sub-sections apply:

| Section | Required | Purpose |
|---|---|---|
| `Request` | **yes** | Fenced code block in the protocol's format (see Protocol shapes). |
| `Checks` | no | `jq`-style assertions run against the response body after a Run. |
| `Mock` | no | ` ```json ` block served as a local stub for offline development. |
| `Notebook` | grows over time | Append-only log of runs, caveats, and observations. |
| `Base` | no | Per-endpoint base URL override (overrides the frontmatter `env` map for this endpoint only). |

A minimal collection with one endpoint looks like this:

````markdown
---
env:
  prod: https://api.example.com
  staging: https://staging.api.example.com
vars:
  ACCOUNT_ID: ""
---

# Example Collection

## GET /users/:id

**Request**

```http
GET {{env}}/users/{{var:ACCOUNT_ID}}
Authorization: Bearer {{vault:API_TOKEN}}
```

**Checks**

.status == 200
.body.id != null

**Notebook**

- 📋 state 2026-06-28T10:00:00Z — collection scaffolded
````

Frontmatter (`---` YAML block at the top of the file) carries the `env` map and default `vars`.
Per-endpoint `Base:` overrides the active env URL for that endpoint only.

Additional frontmatter keys:

| Key | Values | Effect |
|---|---|---|
| `protocol:` | `rest` (default) \| `graphql` \| `mcp` (reserved v1.1) | Selects the wire protocol for the whole collection. |
| `vault:` | `keychain` (default) \| `age` \| `op` \| `env` | Selects the secret-store backend. Overridden at runtime by `MISSIVE_VAULT_BACKEND=<backend>`. |

`MISSIVE_VAULT_BACKEND` — set this environment variable to override the `vault:` frontmatter key without editing the file (e.g. `MISSIVE_VAULT_BACKEND=env` for CI).

---

## Reference types

References appear inside `Request` blocks and, for GraphQL, inside the associated `**Variables**` ```json``` block; they do not appear in `Checks`, `Mock`, or `Notebook` sections. Three types exist:

| Ref | Holds | Renders? | Resolved by |
|---|---|---|---|
| `{{env}}` | base URL for the selected `--env` | yes | frontmatter `env` map |
| `{{var:NAME}}` | non-secret input | yes | `--var` → frontmatter `vars:` → prompt |
| `{{vault:NAME}}` | a secret | **never** | the vault, in a subprocess, at fire-time |

Key distinction: `{{env}}` and `{{var:NAME}}` are rendered into the request string before the
subprocess is spawned. `{{vault:NAME}}` is **never** rendered — the name is passed to
`scripts/vault.sh exec`, which injects the value as an environment variable inside the subprocess
and runs the command there. The secret never touches the agent's text context.

---

## Protocol shapes

### REST

The `Request` block uses a ` ```http ` fence. The agent fires it with `curl`, mapping the HTTP
method, path, headers, and body directly.

```http
POST {{env}}/v1/zones
Authorization: Bearer {{vault:CF_API_TOKEN}}
Content-Type: application/json

{"name": "example.com"}
```

Checks run against the full response (status + parsed body).

### GraphQL

The `Request` block uses a ` ```graphql ` fence. An optional `**Variables**` section holds a
` ```json ` block with query variables. The agent fires this as an HTTP POST to `{{env}}` with
`Content-Type: application/json`, encoding `{"query": "...", "variables": {...}}`. Checks run
against `.data` and `.errors` in the response.

```graphql
query GetUser($id: ID!) {
  user(id: $id) {
    id
    email
  }
}
```

**Variables**

```json
{"id": "{{var:USER_ID}}"}
```

### MCP

MCP protocol support is reserved for v1.1. Do not use a ` ```mcp ` block in v1 collections.

---

## The vault rule

**The law:** the Markdown holds references, never values. A secret must never appear in a `.api.md`
file, a diff, or this transcript.

### Canonical incantation — firing an authenticated request

```sh
scripts/vault.sh exec CF_API_TOKEN -- \
  sh -c 'curl -sS -H "Authorization: Bearer $CF_API_TOKEN" "$URL"'
```

`exec` injects the named secret(s) as environment variables into the subprocess and runs the
command there. The secret value is never returned to the agent's text context.

> **Known limit (v1):** while the call runs, the value is briefly present in process arguments
> (visible to `ps` for the same user on the same machine). It never reaches a **file, diff, or
> transcript** — the boundary this product guarantees. v1.1 closes the `exec` window
> (export-then-`exec`); `set`'s Keychain-write window is inherent to the `security` CLI.

### Prohibitions

1. **Never run a bare `scripts/vault.sh get NAME` and read its output into context.** `get` writes
   the value to stdout for pipe-to-subprocess use only; capturing it in a shell variable and then
   referencing that variable in a command that the agent can observe is a violation.

2. **Never paste a raw response that may echo a secret without redacting known vault values.** If
   an API response contains a value that matches a vault secret, redact it before including any
   portion of that response in context or in the Notebook.

3. **A secret returned by an API goes back to the vault via `set`, never into the file.** Store
   dynamic credentials with `printf %s VALUE | scripts/vault.sh set NAME`, then reference them
   as `{{vault:NAME}}` in subsequent requests.

### Vault CLI reference

| Command | What it does |
|---|---|
| `scripts/vault.sh get NAME` | Writes value to stdout. For subprocess injection only — do not capture into agent context. |
| `printf %s VALUE \| scripts/vault.sh set NAME` | Stores a secret (value from STDIN). |
| `scripts/vault.sh list` | Prints stored secret names only (no values). |
| `scripts/vault.sh exec NAME[,NAME] -- CMD` | Injects secret(s) as env vars, runs CMD in subprocess. The inject-and-fire primitive. |

Secret names must match `[A-Za-z_][A-Za-z0-9_]*`.

### Keychain addressing

By default, `{{vault:NAME}}` reads the macOS Keychain item with **service=`missive`** and
**account=`NAME`**. Two env vars let you remap this — both are templates where `{name}` expands
to the secret name and `{user}` expands to `$(id -un)`:

| Env var | Default | Effect |
|---|---|---|
| `MISSIVE_VAULT_SERVICE` | `missive` | Keychain service field |
| `MISSIVE_VAULT_ACCOUNT` | `{name}` | Keychain account field |

Example — read a token stored as service=the-secret-name, account=your-login (Mark's convention):

```sh
export MISSIVE_VAULT_SERVICE='{name}'
export MISSIVE_VAULT_ACCOUNT='{user}'
```

With those set, `{{vault:CLOUDFLARE_API_TOKEN}}` looks up service=`CLOUDFLARE_API_TOKEN`,
account=`<your-login>`. Unset both to restore the default (`missive` / `NAME`).

Diagnostic — verify resolution without touching the Keychain or reading any secret:

```sh
scripts/vault.sh kc-addr CLOUDFLARE_API_TOKEN
# prints: CLOUDFLARE_API_TOKEN<TAB>markstatkus  (with the env vars above)
```

---

## Running the three modes by hand

This section describes the three modes an agent executes against a collection endpoint.

### Run

1. Parse the collection frontmatter to load the `env` map and default `vars`.
2. Resolve `{{env}}` from the `--env` flag (default: first key in the `env` map).
3. Resolve each `{{var:NAME}}` from `--var NAME=value`, then frontmatter `vars:`, then prompt the
   user if still unset.
4. Identify each `{{vault:NAME}}` reference; do **not** render them.
5. Build the `curl` command for the protocol, substituting rendered values; leave vault references
   as env-var names (`$NAME`).
6. Fire via `scripts/vault.sh exec NAME[,NAME] -- <curl-command>`.
7. Capture HTTP status and response body; report both to the user.

### Check

After a successful Run, evaluate each line of the `Checks` block as a `jq` expression against the
response body.

- Report `PASS` or `FAIL` for each check.
- On `FAIL`, show the failing `jq` path and the actual value. Redact the value if it matches any
  known vault secret name.

### Mock

For each endpoint that has a `Mock` block:

1. Parse the `Mock` JSON block.
2. Spin up a throwaway local HTTP server (e.g. a short `python3 -m http.server` handler or an
   `nc`-based responder) that serves the mock JSON on the endpoint's route.
3. Report the local URL to the user for offline development against the stub.
4. Shut down the server when the user is done or the session ends.

The Mock mode does not require vault access and works fully offline.

---

### Authoring across shapes & auth

Every API — MCP-style primitive or sprawling human-shaped — is expressed in the one shape:
`Request` / `Checks` / `Mock` / `Notebook`, one capability per `##`. Only the Request fence and the
auth line vary. Copy `MISSIVE-TEMPLATE.md`, then pull a Shape recipe and an Auth recipe from
`MISSIVE-PATTERNS.md`. Don't invent structure; if it isn't a listed recipe, it still collapses into
those four sections. Secrets stay `{{vault:NAME}}`, fired via `scripts/vault.sh exec`.

---

### Advanced auth (runner-driven)

Three auth families are computed deterministically by `scripts/missive-run.sh`, not by hand:
**pre-request/Capture** (OAuth2, session login — an ordinary `##` endpoint gains a `**Capture**`
section; the main request references `{{capture:NAME}}`), **signed requests** (a `**Sign**` block —
HMAC over an `over:` template with `{{sign:timestamp}}`, or `scheme: sigv4`), and **mTLS** (`tls:`
frontmatter with `{{vault:}}` cert/key). Advanced-auth endpoints are **runner-only**:
`missive-run.sh <file> "<endpoint>" --env <env>` emits a **redacted** verdict — secrets never render
into the file, the diff, or the transcript, and the body channel is allowlisted to the Checks.
Copy the matching recipe from `MISSIVE-PATTERNS.md`.

Two things worth knowing before you copy a recipe:

- **The SigV4 signing engine is one global, offline, fail-closed choice — never per-request.** The
  default (auto) engine is `openssl`, used only after it self-certifies against the vendored KAT
  vectors on this host; the runner refuses to sign at all if it can't certify. **Signing-key argv
  window:** the openssl path passes the derived signing key to `openssl dgst` on argv for a
  ~millisecond window, readable by a same-user or root process (`ps`/procfs) — the same local blast
  radius that can already read your vault, which is why the default is accepted rather than changed.
  curl-native signing is argv-clean but not offline-KAT-certifiable, so it's opt-in for when you want
  to close that window: `MR_SIG_ENGINE=curl`. **HMAC signing is always openssl** (LibreSSL's
  `openssl dgst` has no keyfile input), so its key rides argv too, with no opt-out — reserve HMAC
  secrets for hosts whose same-user/root processes you already trust with the vault.
- **The Capture `(cache ...)` hint is active.** With `NAME <- .path (cache .expires_in)` (TTL from a
  response field, minus a ~10%/60s safety margin) or `(cache 3300s)` (literal, as-written), the runner
  reuses a fetched value across runs instead of re-firing the producer. The cache lives in
  `$XDG_CACHE_HOME/missive/` (`0700` dir, `0600` files), keyed by a salted fingerprint of the resolving
  request, so rotating the secret invalidates it. A producer carrying `{{sign:timestamp}}` or another
  `{{capture:}}` is not cache-safe (it warns and refetches). No hint → ephemeral (refetch every run), the default.

---

## Maintaining the Notebook

Every endpoint block may have a `**Notebook**` sub-section. It grows by append only — never delete
or edit existing lines.

### Entry shape

```
- <emoji> <bucket> <timestamp> — <text>
```

- **bucket** is one of: `caveat | state | outcome`
- **timestamp** is ISO-8601 (`YYYY-MM-DD` minimum; prefer `YYYY-MM-DDThh:mm:ssZ` UTC)
- **emoji** is a single character that suits the entry (📋 for state, ⚠️ for caveat, ✅ for outcome
  are conventional but not mandatory)

### When to append

| Trigger | Bucket | Example |
|---|---|---|
| A Run completes successfully | `state` | `- 📋 state 2026-06-28T14:32:00Z — GET /users/42 → 200 OK` |
| Something unexpected or surprising during a run | `caveat` | `- ⚠️ caveat 2026-06-28T14:33:00Z — rate-limit header absent on staging` |
| A run proved or observed something structural about the schema | `outcome` | `- ✅ outcome 2026-06-28T14:35:00Z — confirmed .data.user.id is always a string` |

Always use UTC. Never back-date. Never edit a prior entry.

---

## Authoring a new collection

To start a new collection, copy `MISSIVE-TEMPLATE.md` to `<name>.api.md` and fill it in; it carries the canonical structure and the vault rule inline.
