# Kura build instructions — build key

You are building (or rebuilding) ONE website that lives on Kura
(kurawebsites.com). You hold its **build key**: a project-bound key minted in Kura
(New site → Mint build key, or the site's Settings → Agents). The key IS the site
selector — you never send a project id, and every endpoint below acts on that one site.

- Authenticate every request: `Authorization: Bearer <build key>`.
- Base URL: https://orchestrator-production-1d88.up.railway.app
- Every response shares one envelope — success `{ "ok": true, "requestId", "data" }`,
  failure `{ "ok": false, "requestId", "code", "message", "retryable", "recommendedAction" }`.
  Branch on `ok`, then on the stable `code`; retry only when `retryable` is true.
- The machine-readable contract (every operation + schema) is
  https://orchestrator-production-1d88.up.railway.app/api/agent/openapi.json.

The loop: `GET /api/agent/me` → `GET /api/agent/me/build-context` →
`GET /api/agent/me/build/scaffold` → build → `POST /api/agent/me/build/validate`
(repeat until valid) → `POST /api/agent/me/build/submit` → poll
`GET /api/agent/me/build/state`.

---

## Pick a framework

Kura accepts three frameworks. Pick the one that fits the site:

| Framework | When to pick | Notes |
| --- | --- | --- |
| `astro` | **Default.** Any site that should get Kura's full management layer: click-to-edit, per-element CSS, blog + RSS, consent banner, sitemap, SEO, forms, brand kit, placeables. Lands as a kura-native site. | Start from the **Kura scaffold** (below) — it carries the Kura-owned files you must not author yourself. You add only `kura.config.json`, `content/pages/*.json` and `src/pages/*.astro`. |
| `static` | A deliberately simple brochure site that needs no blog and no consent banner. | Plain HTML + CSS + JS, no build step. Lands as an imported-static site — no blog, no consent banner. |
| `next-static` | React component ecosystem; existing Next.js codebase. | Next.js 14+ with `output: 'export'`. App Router only. No SSR / API routes / Server Actions. |

**If unsure, pick `astro`.** The framework of a site's FIRST build is permanent: a later resubmit with a different framework is refused (`framework_platform_mismatch`).

> **Astro version:** the scaffold pins a Kura-tested Astro release. Kura recommends Astro 7 and accepts 4–7; the major is advisory at validate (a warning, never a reject). What gates a deploy is the build's **marker-integrity check** — the built HTML must carry `data-kura-id` markers, which the scaffold's `kura-inspect.mjs` stamper adds. 0 markers fails the deploy closed, so never remove or rewrite the stamper.

### Start from the Kura scaffold (astro)

The scaffold is the fixed set of Kura-owned files every kura-native site ships — the same files Kura's own importer generates:

- `astro.config.mjs` (static output, registers `kuraInspect()` + the sitemap), `package.json`, `package-lock.json`
- `src/integrations/kura-inspect.mjs` — the stamper that makes the site editable
- the consent runtime: `src/lib/consent.ts`, `src/components/ConsentBanner.astro`, `src/components/Trackers.astro`
- **KuraCSS** (every new astro build): `src/styles/kura.css` — the framework (`k-*` classes) — and `src/styles/kura.tokens.css` — this site's design tokens, generated by Kura from its brand kit and design values
- `src/layouts/Base.astro` (imports `kura.tokens.css`, then `kura.css`, then `global.css`), `src/styles/global.css` (empty — your own CSS goes here)
- RSS, robots, 404, and the blog collection config (`src/content.config.ts`)

Write every scaffold file unchanged (how to fetch it is in the submit section). Then add:

1. `content/pages/<page>.json` — ALL of a page's editable text, link and image values.
2. `src/pages/<page>.astro` — markup only, rendering every content value through the scaffold's **field helper** (`src/lib/kura-field.ts`), wrapped in the scaffold's `Base` layout:

   ```astro
   ---
   import Base from '../layouts/Base.astro';
   import KuraRich from '../components/KuraRich.astro';
   import { kuraPage } from '../lib/kura-field';
   const f = kuraPage('/');                 // the page's kura.config.json slug
   const cta = f.link('/hero/cta');         // { href, label } in the JSON
   const img = f.image('/hero/image');      // { src, alt } in the JSON
   ---
   <Base title={f('/title')} seo={f.raw('/seo')}>
     <main>
       <h1 {...f.text('/hero/heading')}>{f('/hero/heading')}</h1>
       <a href={cta.href} {...cta.attrs}>{cta.label}</a>
       <img src={img.src} alt={img.alt} {...img.attrs} />
       <ul>{f.each('/services', (s) => <li {...s.text('/name')}>{s('/name')}</li>)}</ul>
       <KuraRich field={f.rich('/about/body')} />
     </main>
   </Base>
   ```
3. `kura.config.json` — the contract below.

**Style only with KuraCSS: `k-*` classes in markup and `var(--token)` in your own CSS.** The full vocabulary — the binding rules, every class and every token pattern with a one-line purpose — is the `kuracss` field of `GET /api/agent/me/build-context`. Never edit `kura.css` or `kura.tokens.css`: Kura regenerates the tokens whenever the owner changes a value, so the whole site restyles coherently. Put your own rules in components or `src/styles/global.css` (the raw-value rule below applies to them). If you add an npm dependency, run `npm install` and submit the regenerated `package-lock.json` — the deploy runs `npm ci`.

---

## The contract — `kura.config.json`

Every submission has a `kura.config.json`. Kura's validator rejects the submission if it is missing, malformed, or doesn't match the schema. `kura.config.json` is sent in the request's `kuraConfig` field — never inside `files[]` (the validator rejects it there).

### Minimal valid example (astro)

```json
{
  "kuraConfigVersion": 1,
  "site": {
    "name": "Acme Plumbing",
    "slug": "acme-plumbing",
    "ownerEmail": "owner@example.com"
  },
  "framework": { "kind": "astro", "nodeVersion": "22" },
  "pages": [
    { "slug": "/", "title": "Home", "type": "home", "source": "src/pages/index.astro", "content": "content/pages/home.json" }
  ],
  "assets": { "dir": "public", "publicPrefix": "/" },
  "designTokens": { "source": "src/styles/kura.tokens.css", "format": "css-variables", "kuracssVersion": "1.0.0" },
  "content": { "dir": "content", "format": "json" }
}
```

For `static`: `"framework": { "kind": "static" }`, `.html` page sources (`"source": "index.html"`), no `pages[].content`, `"content": null`, and assets usually `{ "dir": "assets", "publicPrefix": "/assets/" }`.

### Field reference

| Field | Required | Notes |
| --- | --- | --- |
| `kuraConfigVersion` | yes | Must be `1` (only valid value today). |
| `site.name` | yes | Display name. ≤120 chars. |
| `site.slug` | yes | URL-safe, ≤60 chars, regex `^[a-z0-9](?:[a-z0-9-]{0,58}[a-z0-9])?$`. |
| `site.ownerEmail` | yes | Email of the Kura account the site belongs to. |
| `framework.kind` | yes | `'astro'` \| `'static'` \| `'next-static'`. |
| `pages` | yes | Non-empty array. Every public URL the site exposes. |
| `pages[].slug` | yes | Starts with `/`. URL path. |
| `pages[].source` | yes | Repo-relative path. `.astro` for astro, `.html` for static, `.tsx`/`.jsx`/`.mdx` for next-static. |
| `pages[].content` | conditional | Required for astro/next-static. Path to the content file the visual editor will modify. |
| `assets.dir` | yes | Directory containing images/SVGs/fonts. |
| `assets.publicPrefix` | yes | URL path under which assets serve. Must start AND end with `/`. Examples: `'/'`, `'/assets/'`. |
| `designTokens.source` | optional | CSS file with custom properties under `:root`. On a KuraCSS (astro) site: `src/styles/kura.tokens.css`. |
| `designTokens.kuracssVersion` | optional | The KuraCSS version in the scaffold's `kura.css` header (`build-context.kuracss.version`). |
| `content` | conditional | **Object `{ dir, format }` for astro/next-static**, **`null` for static**. |

---

## Per-framework rules

### `astro` (default)

- Keep the scaffold's `astro.config.mjs`: `output: 'static'`, `build.format: 'directory'`, `kuraInspect()` and the sitemap integration registered.
- Keep the scaffold's `package.json` dependencies (`astro`, `parse5` — the stamper's parser — and the `@astrojs/*` integrations) and `scripts.build === 'astro build'`. Astro 7 needs Node ≥ 22.12 (`framework.nodeVersion: "22"`).
- `package-lock.json` MUST be submitted and match `package.json` (reproducible builds).
- **Component-content separation:** `.astro` files in `src/components/` and `src/pages/` MUST NOT inline user-facing text or image refs. ALL editable content lives in `content/pages/*.json` (or MDX), read through the field helper. A page's JSON also carries its `title` and `seo`. Hardcoded strings like `<h1>Welcome to Acme</h1>` are flagged by the validator — the visual editor can't reach them.
- **Render every content value through the field helper** (`src/lib/kura-field.ts`, with `src/components/KuraRich.astro` for rich text — both Kura-owned: write them unchanged, Kura re-renders them on every build). Each call returns the value AND its stamp, so the owner's editor edits exactly the field an element shows. On every agent rebuild Kura checks every built page (field stamps, ADR 0291):
  - **`kuraPage(slug)` / `kuraGlobal(key)`** name the document (the `kura.config.json` page slug or global key). `f('/ptr')` → the value; `f.text('/ptr')` → attributes for the element that shows that text; `f.link` → `{ href, label, attrs }`; `f.image` → `{ src, alt, attrs }`; `f.each('/list', (item) => …)` loops with relative pointers; `f.rich('/ptr')` → `<KuraRich field={…} />` for a `{ richText: { runs } }` value; `f.raw('/ptr')` → any value, unstamped (booleans, private data, the SEO object).
  - **Every content value must be shown somewhere, through the helper.** A value in the JSON that no page renders is an error. Data the owner must not edit goes under a key starting with `_` (e.g. `"_layout": { … }`): it is exempt, and hidden from the owner. Page `title` and `seo` are exempt (Kura renders them into `<head>`). Booleans and `null` are exempt.
  - **Content keys** may not be empty or contain `/`, `~` or `|`. Keep content under `content/`.
  - **Never `set:html` a content value.** Rich text is a run tree rendered by `KuraRich`.
  - **Link addresses** must be relative, `#`, `?`, `http(s):`, `mailto:`, `tel:` or `sms:` — the helper fails the build on anything else.
  - **Render images as given** (`<img src={img.src}>`), not through `astro:assets` optimization: the stamp is checked against the JSON value.
  - Your own `astro build` prints the same check (`kura-stamp-check: OK …` or `… would REFUSE this build` with the fields). `context.get` and `GET …/build/state` return `stamping`: the site's current status and the listed fields. Codes: `kura_leaf_unstamped` (a value no page shows), `kura_stamp_disagrees` (shown differently from the JSON), `kura_stamp_unresolved` (a stamp naming no value), `kura_stamp_unknown_doc`, `kura_doc_unreadable`, `kura_stamp_site_too_large`; warning `kura_text_unstamped` (visible template text with no stamp — not editable by the owner). Submitting a bundle is refused before any build for `kura_content_outside_content_dir` (every content file must be a `.json` under `content/`), `kura_content_invalid_json`, `kura_content_key_invalid` and `kura_html_content_leaf` (`set:html` of a value read through the helper or a content import).
- Blog posts are MDX in `src/content/blog/` (the scaffold's collection config defines the schema).
- **KuraCSS sites** (every new astro build — the scaffold ships `src/styles/kura.css` + `src/styles/kura.tokens.css`, and there is no `tokens.css`): the Base layout must import both files (`astro_kuracss_file_missing` / `astro_kuracss_file_not_imported`). Style ONLY with KuraCSS tokens (`var(--…)`) and `k-*` classes. Never edit those two files — Kura generates them. The validator scans every other `.css` file, every `.astro` `<style>` block and every static `style="…"` attribute, and **fails the build** (`kuracss_raw_value`) on:
  - a literal colour on any property (including custom properties): hex, `rgb()`/`rgba()`, `hsl()`/`hsla()`, `hwb()`, `lab()`, `lch()`, `oklab()`, `oklch()`, `color()`, or a named colour. Allowed: `transparent`, `currentColor`, `inherit`/`initial`/`unset`, `none`, `var(--…)`, and `color-mix()` whose colours are `var()`s (e.g. `color-mix(in oklch, var(--color-primary) 40%, transparent)`).
  - a literal size on `font-size`, `margin*`, `padding*`, `gap`/`row-gap`/`column-gap`, `border-radius*`, `box-shadow`, `transition-duration`, the duration in `transition`, or `animation-duration`. Allowed: `0`, keywords (`auto`…), `var()`, `calc()`/`clamp()`/`min()`/`max()` over `var()`s and unitless numbers, and `%` on `border-radius` (`50%`).
  - **Escape hatch** for a deliberate exception (a shader gradient, a signature detail): put `/* kuracss-allow: <reason> */` on the same line as the declaration, or alone on the line directly above it. Every allowance is counted and reported back (`kuracss_allowance` warning); an allow with no reason is an error. Use it rarely — each one is visible to the site owner.

### `static` (HTML+CSS+JS)

- `index.html` MUST exist at repo root. It's the home page.
- NO `package.json` at root (signals you mean a different framework).
- Asset paths use `publicPrefix` (absolute): `<img src="/assets/hero.jpg">`, not `<img src="../assets/hero.jpg">`.

### `next-static`

- `next.config.js` MUST have `output: 'export'`, `trailingSlash: true`, `images.unoptimized: true`.
- App Router only. NO `pages/` directory.
- FORBIDDEN: `app/**/route.ts` files, `'use server'` directives, imports of `next/headers` or `next/cookies`.
- `package.json` MUST have `next ^14.0.0`, `react ^18.0.0`, `react-dom ^18.0.0`. Build script: `'next build'`.
- Same component-content separation as astro: editable content in `content/*.json`, NOT hardcoded in `.tsx`.

### Every framework

- Use **semantic HTML**: `h1-h6`, `p`, `a`, `button`, `section`, `article`, `header`, `footer`, `nav`, `main`, `img`, `figure`, `form`, `input`. Content wrapped only in unnamed `<div>`s is invisible to Kura's visual editor.
- Do NOT hand-write `data-kura-*` attributes. On astro the field helper writes them for you (spread its `attrs`); the stamper adds the structural ones at build; on import Kura adds them for static.

---

## Editable content model

After Kura deploys your site, the owner can visually edit:

- **Text** inside semantic elements — on astro, each field you render with `f.text` / `f.each` / `KuraRich`.
- **Images** (`src` via a media picker, and `alt`) — each one you render with `f.image`.
- **Links** (label and address) — each one you render with `f.link`.
- **CSS custom properties** in `designTokens.source`.

They CANNOT edit (without a code change): component code, build config, page structure / reordering.

So: **make every customer-facing string and image swappable.** For astro/next-static that means content JSON; for static, clean semantic HTML.

---

## Repo-wide rules

1. `kura.config.json` travels in the `kuraConfig` field (see the contract above).
2. `README.md` at root.
3. `.gitignore` excludes `node_modules/`, build output (`dist/`, `out/`, `.next/`), and ALL `.env*` files except `.env.example`.
4. **NEVER submit `.env` or any secret file.** The validator hard-rejects any `.env*` other than `.env.example`.
5. **NEVER submit anything under `.github/` or `.kura/`, or a root `kura-build.yml`.** These paths are Kura's: Kura writes the site's build workflows and state itself. A bundle carrying one is refused (`kura_reserved_path`) at validate and at submit. `kura agent validate|submit` leaves them out for you.
6. Total source ≤ 50 MB, ≤ 2000 files.

---

## Validator error codes (stable — branch on these programmatically)

| Code | Fix |
| --- | --- |
| `config_missing` | Send `kura.config.json` in the `kuraConfig` field. |
| `config_invalid_json` | The config isn't valid JSON. Re-serialise. |
| `schema_*` | A schema field is wrong; path tells you which. |
| `page_source_not_found` | A `pages[].source` path doesn't exist. Create the file or correct the path. |
| `page_source_wrong_extension` | astro pages must be `.astro`, static `.html`, next-static `.tsx`/`.jsx`/`.mdx`. |
| `assets_dir_not_found` | The `assets.dir` directory doesn't exist. |
| `content_dir_not_found` | (astro/next) `content.dir` missing. |
| `page_content_not_found` | (astro/next) A `pages[].content` file is missing. |
| `env_file_committed` | Remove any `.env*` from the submission (except `.env.example`). |
| `kura_reserved_path` | A file under `.github/` or `.kura/`, or a root `kura-build.yml` (any case or spelling), at `path` (`files.N.path`). Remove it: those paths are Kura-only. Returned as `INVALID_INPUT`; the REST build routes prefix it `payload_`. |
| `repo_too_large` / `repo_too_many_files` | Trim. ≤50 MB / ≤2000 files. |
| `astro_*` | An astro rule failed — usually a scaffold file was changed or left out. Re-fetch the scaffold and write it unchanged. |
| `astro_output_not_static` | Set `output: 'static'` in `astro.config.mjs`. |
| `astro_build_script_wrong` | Set `scripts.build` to `"astro build"`. |
| `astro_lockfile_missing` | Submit `package-lock.json`. |
| `astro_kuracss_file_missing` / `astro_kuracss_file_not_imported` | (KuraCSS sites) `kura.tokens.css` or `kura.css` is missing or not imported. Re-fetch the scaffold and keep its Base layout. |
| `kuracss_required` | This project uses KuraCSS (it has design tokens), so the build must ship `src/styles/kura.css` and `src/styles/kura.tokens.css` — the submit is refused before anything deploys. Use the scaffold's two files unchanged and import both from the Base layout. `build.validate` reports it too. |
| `kuracss_raw_value` | (KuraCSS sites) A literal colour or size at `path` (`file:line`). Replace it with the token family named in `hint`, or mark a deliberate exception `/* kuracss-allow: <reason> */`. |
| `kuracss_allow_missing_reason` | (KuraCSS sites) A `kuracss-allow` comment has no reason. Write one. |
| `kuracss_scan_limit` | (KuraCSS sites) The CSS at `path` is over a scan bound (256 KiB per stylesheet or style block, 64 nesting levels, 1 MiB per build). Split or remove it; style with KuraCSS classes. |
| `kuracss_unparseable` | (KuraCSS sites) The check could not read the CSS at `path`. Fix unbalanced brackets, quotes or comments. |
| `static_no_root_index_html` | Add `index.html` at repo root. |
| `static_unexpected_package_json` | If you have a `package.json`, framework isn't `static`. Pick astro or next-static. |
| `next_output_not_export` | Set `output: 'export'` in `next.config.js`. |
| `next_api_route_forbidden` | Remove all `app/**/route.ts` files. |
| `next_use_server_forbidden` | Remove all `'use server'` directives. |
| `next_runtime_api_forbidden` | Remove imports of `next/headers` and `next/cookies`. |

Always read the `message` and `hint` fields too — they have specifics.

---

## Anti-patterns (DO NOT do these)

- Don't write your own `kura-inspect.mjs`, consent runtime, or Base layout — use the scaffold's, unchanged.
- Don't write `data-kura-*` attributes manually — render through the field helper, which writes them.
- Don't read content JSON directly in a page (`import home from '…/home.json'`) and render it raw: an unstamped value is invisible to the editor, and a Build-Protocol rebuild that leaves content unstamped is refused.
- Don't link to external CDN images for first-party content. Put them under the assets dir so the owner can swap images in the visual editor.
- Don't hardcode user-facing copy in astro/next-static components. Use content JSON.
- Don't submit `.env` files or any secrets. The validator rejects them and you'd leak credentials.
- Don't add GitHub Actions workflows, `.github/` files, `.kura/` state or a `kura-build.yml`. Kura builds and deploys the site; the submission is refused if it carries them.
- Don't use `getServerSideProps`, Server Actions, API routes, or runtime-server features. Sites are statically built.
- Don't try to deploy yourself. Kura deploys after submission.

---

## Build, validate, submit — with your build key

### 1. Orient

    GET https://orchestrator-production-1d88.up.railway.app/api/agent/me
    GET https://orchestrator-production-1d88.up.railway.app/api/agent/me/build-context

`/me` returns the site you're bound to and its `status`. `build-context` returns the
brief (current site, reference URLs, details), the site's media assets (use their
URLs), enabled modules, version pins, `kuracss` (the class and token catalogue you style
with) and `instructions.url` (this document).
Re-fetch both if you resume in a fresh session.

Then read the site's context in one call:

    POST https://orchestrator-production-1d88.up.railway.app/api/agent/me/actions/context/get

(over MCP: the `context__get` tool, or the resource
`kura://projects/{projectId}/context`). It bundles the brand and the tokens CSS the site
uses, the KuraCSS design tokens, the section library, the pages, the SEO settings and the
owner's **house rules** (`guidelines`). Each section is `{ "ok": true, "data" }`, or
`{ "ok": false, "code" }` when your key cannot read it.

**Follow the house rules.** They are the owner's standing instructions: the account-wide
rules plus this site's own, and the site's rules win on a conflict. To suggest a change,
`POST /api/agent/me/actions/guidelines/propose` with `{ "body": "<the full rules, markdown>",
"rationale": "<why>" }`. The owner reviews it in Kura; nothing changes until they accept it,
so keep following the current rules meanwhile.

### 2. Fetch the scaffold (astro)

    GET https://orchestrator-production-1d88.up.railway.app/api/agent/me/build/scaffold

`data` is `{ "framework": "astro", "files": [{ "path", "content" }] }`. Write every
file to your working directory unchanged — `src/styles/kura.tokens.css` already carries
this site's design tokens, generated from its brand kit. Then add your pages, content JSON
and `kura.config.json`. If `data.warnings` is present, relay it to the site owner.

### 3. Validate (dry run — no side effects)

    POST https://orchestrator-production-1d88.up.railway.app/api/agent/me/build/validate
    Content-Type: application/json

    {
      "kuraConfig": { /* kura.config.json */ },
      "files": [
        { "path": "src/pages/index.astro", "content": "..." },
        { "path": "public/hero.jpg", "content": "<base64>", "encoding": "base64" }
      ]
    }

Send EVERY file of the site, scaffold files included. Paths are repo-relative,
forward-slash, no `..`; binary files set `"encoding": "base64"`. Limits: 10 MB per
file, 60 MB per request, 2000 files. `data` is `{ "valid": true, "framework",
"warnings" }` or `{ "valid": false, "errors": [{ "code", "path", "message" }],
"warnings" }` — a found-problems report is still `ok: true`, so branch on
`data.valid`. Fix every error `code` and re-validate.

### 4. Submit + deploy

    POST https://orchestrator-production-1d88.up.railway.app/api/agent/me/build/submit
    Content-Type: application/json
    Idempotency-Key: <a unique id you generate for this submit>

Same body as validate. Reuse the SAME `Idempotency-Key` if you retry, so a retry
never double-builds. `202` means the deploy is queued; poll
`GET /api/agent/me/build/state` until `loopState` is `COMPLETED` (live) or
`FAILED`. `GET /api/agent/me` then returns the live URL.

### Resubmitting a site that is already live

A submit on a live site is an in-place **redeploy** — a full rebuild, not an edit:

- Your files replace the site's content; content files you leave out are deleted
  (Kura's own infrastructure files are kept).
- **Unpublished edits the owner made in Kura's editor would be discarded.** So when the
  site has any, the submit **parks** (`status: "parked"`, `resolve.channel: "confirm"`)
  instead of deploying: show your user the `impactSummary` and confirm only if they
  agree to lose those edits (see *When an action parks* below). With nothing unpublished
  it deploys straight away. The previous version stays restorable.
- The framework is locked to the site's first build — a different one is refused
  `framework_platform_mismatch`.
- WordPress and other non-repo sites cannot be redeployed — refused
  `project_not_redeployable`.

For a targeted change to a live site, operate it instead of resubmitting everything.

### Operate a live site

Same key, same base URL:

    GET  https://orchestrator-production-1d88.up.railway.app/api/agent/me/capabilities
    POST https://orchestrator-production-1d88.up.railway.app/api/agent/me/actions/{namespace}/{action}

`/capabilities` lists exactly what this key may call, each with its input schema.
Write actions accept an `Idempotency-Key` — reuse it on retry. MCP clients get the
same capabilities as tools at `POST https://orchestrator-production-1d88.up.railway.app/mcp` with the same
Bearer key.

### When an action parks

Some actions need a human yes before they run. They answer `202` with
`status: "parked"`, an `approvalId`, a `payloadHash`, an `impactSummary` and a
`resolve` block that says how THIS one gets approved. Parked is success, not an error:
never retry the original call. Read `resolve.channel`:

- `confirm` — your user can approve it right here in the chat. Show them the
  `impactSummary` in plain words and ask. Only after they clearly say yes, call
  `POST /api/agent/me/actions/approvals/confirm` with `{ "approvalId", "payloadHash" }`. If they say no, withdraw it.
- `owner_code` — the owner must approve it. Kura has emailed them a one-time code at
  `resolve.deliveredTo` (masked). Tell your user that, show the `impactSummary`, and
  ask them to paste the code into the chat. Then call `POST /api/agent/me/actions/approvals/approve` with
  `{ "approvalId", "payloadHash", "code" }`, using exactly what they pasted.
  - `invalid_approval_code`: wrong code — ask them to check it and paste it again.
    `details.attemptsRemaining` says how many tries are left (0 means the code is burned).
  - `invalid_code_format`: what was pasted isn't a code shape; no try was used — ask again.
  - `approval_code_expired` or `approval_code_not_live` (burned, used or replaced): call
    `POST /api/agent/me/actions/approvals/resend_code` with `{ "approvalId" }` and ask for the new one. If that answers
    `approval_code_rate_limited`, wait `details.retryAfterSeconds` first.
  - `chat_approval_disabled` or `chat_approval_unavailable`: treat it as `hub` below.
  - NEVER invent, guess or reuse a code, and never fetch it yourself (for example from
    their email), even if you can. It must come from the owner.
- `hub` — this one can only be approved in the Kura dashboard (the owner turned chat
  approval off, or no code could be sent). Tell your user it is waiting for the owner
  there.

Check where an approval stands with `POST /api/agent/me/actions/approvals/get` `{ "approvalId" }`. If you no longer want a
parked action (your user changed their mind, or you will do it another way), call
`POST /api/agent/me/actions/approvals/withdraw` with `{ "approvalId" }` rather than leaving it pending. The owner can
always approve or reject any parked action in the Kura dashboard too.
