# Author a playbook with an LLM coding tool

The fastest way to write a playbook is to let your coding assistant (Claude Code, Cursor,
etc.) draft it, then validate and sign it with the CLI. This page is written to be handed
*to that assistant* — point it here, describe the flow you want, and iterate against
`obep lint` until it's clean.

> A **playbook** is a signed JSON recipe for one browser flow. It carries a **fence**
> (exactly which domains, actions, and credential keys are allowed) and an optional
> deterministic **steps** list. Secrets are never in the playbook — only key *references*
> resolved from the user's on-device vault.

## The closed action set

These are the only 8 actions. Adding one means shipping a new extension version — an LLM
cannot invent actions.

| action | fields | does |
|---|---|---|
| `navigate` | `url` | go to a URL |
| `click` | `selector` | click an element |
| `fill` | `selector`, `value` | type a **non-secret** value |
| `fillSecret` | `selector`, `credentialKey` | type a secret resolved from the vault (value never in the playbook) |
| `upload` | `selector`, `file` | attach a file |
| `wait` | `selector?`, `timeoutMs?` | wait for an element / timeout |
| `extract` | `selector`, `as` | read a value into the result under key `as` |
| `done` | — | finish |

## The shape

```jsonc
{
  "playbookId": "acme.portal-upload",        // ^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$
  "version": 1,
  "tenantId": "acme",
  "permissions": {                            // the FENCE — keep it as tight as the flow needs
    "allowedDomains": ["portal.example.com"], // exact hosts, or single-label wildcard "*.example.com". No bare "*".
    "allowedActions": ["navigate", "fillSecret", "click", "upload", "extract"],
    "allowedCredentialKeys": ["portal_username", "portal_password"], // ^[a-z0-9_]+$ — refs, never values
    "capture": { "screenshots": "never", "fullDom": false, "elementsOnly": true },
    "sensitiveSurfacePolicy": "block"         // or "acknowledge" + "acknowledgeSensitive": ["..."]
  },
  "steps": [                                  // optional deterministic happy-path; omit for pure-LLM
    { "action": "navigate", "url": "https://portal.example.com/login" },
    { "action": "fillSecret", "selector": "#username", "credentialKey": "portal_username" },
    { "action": "fillSecret", "selector": "#password", "credentialKey": "portal_password" },
    { "action": "click", "selector": "#signin" },
    { "action": "navigate", "url": "https://portal.example.com/claims/upload" },
    { "action": "upload", "selector": "input[type=file]", "file": "report.pdf" },
    { "action": "click", "selector": "#submit" },
    { "action": "extract", "selector": ".confirmation-number", "as": "confirmationNumber" }
  ],
  "fallback": "halt"                          // "halt" = stop if a step breaks; "llm" = hand off to your decider
}
```

`issuedAt` and `signature` are added by the CLI at sign time — the assistant does **not**
write them.

## Rules the assistant must follow (these are lint errors otherwise)

- **Secrets only via `fillSecret` + `credentialKey`.** Never put a password in a `fill`
  `value` or anywhere in the file. Credential keys are references to the user's vault.
- **Tightest possible fence.** `allowedDomains` = only the hosts the flow touches (exact
  where you can); `allowedActions` = only the actions used; `allowedCredentialKeys` = only
  the keys referenced. Over-broad fences are flagged.
- **No bare `*` domain**, no bare TLD. Single-label wildcards like `*.example.com` are allowed.
- **Default-tight capture:** `screenshots: "never"`, `fullDom: false`, `elementsOnly: true`.
  Anything looser must be justified and is flagged.
- **`sensitiveSurfacePolicy: "block"`** unless a surface is explicitly acknowledged.

## The loop

Have the assistant write `flow.json`, then drive it against the CLI — the linter is the
guardrail, and `sign` **refuses** a playbook with lint errors, so the assistant iterates
until clean:

```bash
npx @obep/cli lint flow.json          # fix every ✖ error the assistant sees, re-run
npx @obep/cli sign flow.json --key keys/tenant.key --out flow-signed.json
```

Then register `flow-signed.json` with the relay; it gets a `playbookId` you call at runtime.

## A prompt you can paste

```
You are writing an OBEP playbook — a signed JSON recipe for one browser flow.
Read docs/PLAYBOOK_AUTHORING.md for the exact schema, the 8 allowed actions, and
the fence rules. Then write flow.json for this flow:

  <describe the flow: the site, the login fields, the steps, what to extract>

Requirements:
- Use ONLY the 8 actions. Secrets go through fillSecret + a credentialKey (e.g.
  portal_password) — never a literal value.
- Make permissions as tight as the flow needs (exact domains, only actions used,
  only credential keys referenced; capture screenshots:never, fullDom:false).
- Do not add issuedAt or signature.

Then run `npx @obep/cli lint flow.json`, fix every error, and repeat until it
reports clean. Do not sign — I hold the signing key.
```

The last line matters: **the human holds the signing key.** An assistant drafts and lints;
a person reviews and signs. See the [Integration guide](./INTEGRATION.md) for the rest.
