# Faithful UI Round-Trip: Import → Edit → Read-Back
Faithful round-trip is the flow for bringing an **already-existing, real UI** into Gakaso with high visual fidelity, letting a human (or you) edit it on the canvas, and then reading the result back out in a form your application can apply — without losing fidelity in either direction. This is distinct from *authoring* new UI from scratch with semantic shapes (see `/canvas/application-mode.md`); faithful round-trip is about **importing what already exists**.
## Why this exists
The scenario: an external application's own agent (you, running against some other app's codebase) wants to hand a live or in-progress UI over to a human for visual polish, or wants Gakaso's design intelligence applied to it, then get the edited result back in a form it can re-apply to the original codebase. The whole pipeline is platform-neutral — it is not tied to HTML/CSS, and is designed so the same neutral representation could in principle be mapped back to native iOS/Android/other targets, not just web.
## The four-step flow
1. **Import** — capture the real UI's neutral visual facts (computed style + geometry) and push them into Gakaso as `gks-frame` objects.
2. **Display** — the imported page renders faithfully in Gakaso from the very first load, no separate "processing" step.
3. **Edit** — a human (or agent) edits the imported UI on the canvas; the design intent is preserved and saves are lossless.
4. **Read-back** — export the current state of the page as a neutral manifest (or as HTML/CSS, or as SwiftUI/Compose-flavored output), which the *importing application's own agent* then applies back to its codebase.
## Import tools
| Tool | Use when | Key parameters |
|---|---|---|
| `gakaso_import_ui_snapshot` | You already have captured UI nodes (neutral visual facts + bounds) — e.g. from your own DOM inspection — and want to persist them as `gks-frame` objects. Platform-neutral (web/native/Figma all feed through the same core). | `projectId`; `nodes` — array (1–2000) of `{ type?, x, y, width, height, text?, src?, style? }`, where `type` is the original semantic/platform type (`"button"`, `"ios-tabbar"`, …) or `"frame"`, and `style` is a loose bag of neutral visual facts; `pageId` (default `"default"` — the page a fresh project's canvas loads); `clear` (boolean — delete existing objects on that page first). |
| `gakaso_import_app_snapshot` | You have an HTML **string** (not a file path) representing the UI and want Gakaso to derive the neutral IR from it and create a new page. | `projectId`; `html` (raw HTML string, required); `pageName` (optional, default `"Imported Page"`). Gakaso runs a built-in tag→type mapping (`nav→nav`, `main`/`body`→`screen`, `section`→`container`, `article`→`card`, `button`/`a`→`button`, `img`→`image`, headings/`p`/`span`/etc.→`text`, `div`→`container`, and so on); `script`/`style`/`head`/`svg` subtrees are dropped, unrecognized tags pass their children through. |
| `gakaso_import_live_url` | You want Gakaso to fetch and capture a live URL itself (headless browser), rather than you capturing/serializing anything. | `projectId`; `url` (public `http(s)` URL — **SSRF-guarded**: `localhost`/`.local`/`.internal`/loopback/link-local/private (`10.x`, `172.16–31.x`, `192.168.x`, `169.254.x`) hosts and non-http(s) protocols are rejected before any fetch happens); `pageId` (default `"default"`); `regionMaxY` (how far down the page to capture, in px, default 1200); `clear` (default `true`). Captures computed style, bounding boxes, and page/element background (including gradients and `background-image` URLs) as neutral facts — this is closer to "computed-CSS snapshot" than a raw screenshot. |
**Important constraint**: none of the import tools accept an arbitrary local file path to read on Gakaso's behalf — HTML must be passed as a string, not as a path (`gakaso_import_app_snapshot`'s `html` field), or fetched server-side from a public URL (`gakaso_import_live_url`). This is a deliberate security boundary (see `/security/model.md`), not a missing feature; don't attempt to work around it by trying to pass filesystem paths.
All three import tools require the `frame:import` API key scope (see the operational note below — this is not the same thing as the "agent" role preset).
## Read-back tool
`gakaso_export_faithful` reads a page's `gks-frame` objects back out. Parameters: `projectId`; `pageId` (default `"default"`); `format`; `includeClassifications` (array of classification levels to *include* — by default `confidential`/`restricted` objects are excluded; see `/security/model.md`). Supported `format` values:
| Format | Output |
|---|---|
| `manifest` (default) | Neutral node manifest: `nodeType` + bounds + visual facts + text. Map each node to your own framework's native component using `nodeType`. |
| `html` | Self-contained, pixel-exact HTML/CSS reflecting the current state (web target). |
| `responsive` | Faithful at the reference width, but reflows to a stacked layout on narrow viewports — use this instead of plain `html` when the importing app needs the result to also work on mobile. |
| `swiftui` | SwiftUI-flavored output. |
| `compose` | Jetpack Compose–flavored output. |
Match `format` to the `targetPlatform` you set on `gakaso_create_project` (see `/mcp/overview.md`) where applicable — e.g. `swiftui` for an `ios` project, `compose` for `android`.
Read-back requires the `frame:export` scope.
## Operational note: getting a key with `frame:import`/`frame:export` in practice
The project's Settings → API Keys screen has a base role preset (**read** / **write** / **agent**) plus two separate toggles, **Faithful import** and **Faithful export**, that append `frame:import`/`frame:export` into the same key's scope list. If a key was issued before these toggles existed (or without checking them), its scopes won't include `frame:import`/`frame:export` and all four faithful round-trip tools will fail with `INSUFFICIENT_SCOPE` (`-32011`) regardless of which role preset was picked — the base role string alone never satisfies this check.
If you hit `INSUFFICIENT_SCOPE` on one of these four tools, don't keep retrying the same call — tell the human the key needs the Faithful import/export toggle(s) enabled (or re-issued with them on), rather than assuming the tool call itself is malformed. A self-hosted/stdio MCP server configured via `MCP_API_KEY`/`MCP_USER_ID` environment variables instead of a DB-backed per-project key is always granted full (`["*"]`) access and isn't affected by this at all.
## Wire contract (for the technically curious)
Import and read-back cross a trust boundary between an external app's agent and Gakaso, so both directions are validated against an explicit schema rather than accepted as-is:
- **Inbound** nodes are validated; any node missing required fields (geometry must be finite; width/height must be non-negative and finite) is rejected at the boundary rather than silently coerced. Successful `gakaso_import_*` calls return `{ pageId, objectCount, rejected, wireVersion, importedAt, ... }` — check `rejected` (a count, not a list) to know if anything didn't make it through; it's not surfaced as an error unless *nothing* valid came through at all.
- **Outbound**: `gakaso_export_faithful` stamps its response with `wireVersion` (currently a single evolving version number, not a per-call negotiated one) alongside `{ projectId, pageId, format, ... , withheld }` — for `format=manifest` the payload also includes `nodeCount` and the `manifest` array itself; for `html`/`responsive` it's `html`; for `swiftui`/`compose` it's `code`. `withheld` reports how many objects were held back by the data-classification gate (see below) — a non-zero value means some content was intentionally not returned to you, not a bug or a partial-failure.
- Both directions require TLS — this boundary is external-facing and is not the same as internal service-to-service traffic inside Gakaso's own infrastructure, which is a separate trust zone.
## What Gakaso does automatically on your behalf at this boundary
- **Text sanitization** — text captured during import (from `gakaso_import_ui_snapshot`'s `text` fields, `gakaso_import_app_snapshot`'s parsed HTML text nodes, and `gakaso_import_live_url`'s captured DOM text) is sanitized against prompt-injection-style content before it's stored.
- **Data-classification gate on read-back** — objects classified `confidential` or `restricted` are withheld from the exported manifest/HTML/code by default (see `/security/model.md` for the classification model); the `withheld` count tells you this happened. Pass `includeClassifications` explicitly if you need to include a broader set (subject to whatever permission the calling key actually has).
- **Audit logging** — every import and export call is recorded (`frame.import` / `frame.export` actions, who/when/project/page/count/format), independent of anything you do yourself; queryable later via `gakaso_query_audit_log` (see `/security/model.md`).
None of this requires action on your part beyond supplying a key with the right scope — it's boundary behavior Gakaso enforces automatically.
## Example: end-to-end round-trip
A concrete sequence for "pull in this live marketing page, let a human polish it, hand back HTML I can drop into my app":
1. `gakaso_import_live_url` with `{ projectId, url: "https://example.com/pricing" }` → returns `{ pageId: "default", count: 47, wireVersion, importedAt }`.
2. Tell the human the page is imported and ready to review/edit in the Gakaso canvas (`https://<gakaso-host>/projects/{projectId}` in application mode) — this step is manual on their side, not something you drive via MCP.
3. Once the human is done (or on a timer / explicit "read it back now" instruction), call `gakaso_export_faithful` with `{ projectId, pageId: "default", format: "html" }` → returns `{ html, withheld }`.
4. If `withheld > 0`, tell the human some objects were excluded by data classification rather than silently treating the output as complete.
5. Apply the returned `html` into your own codebase however is appropriate for your stack — Gakaso does not push it anywhere on your behalf.