# Models

## Error

```json
{
  "type": "object",
  "properties": {
    "error": {
      "type": "string"
    }
  },
  "required": [
    "error"
  ]
}
```

## FingerprintEnvelope

```json
{
  "type": "object",
  "properties": {
    "fingerprint": {
      "$ref": "#/components/schemas/Fingerprint"
    }
  },
  "required": [
    "fingerprint"
  ]
}
```

## Fingerprint

```json
{
  "type": "object",
  "description": "Native FingerprintConfig. Documented sub-objects are the common ones; the object is open and stored verbatim by the create endpoints, so any field present here is round-tripped.",
  "additionalProperties": true,
  "properties": {
    "name": {
      "type": "string"
    },
    "notes": {
      "type": "string"
    },
    "seed": {
      "type": "integer",
      "description": "0 = overrides-only mode (the common case)."
    },
    "navigator": {
      "type": "object",
      "additionalProperties": true,
      "properties": {
        "user_agent": {
          "type": "string"
        },
        "platform": {
          "type": "string",
          "example": "Win32"
        },
        "hardware_concurrency": {
          "type": "integer",
          "example": 12
        },
        "device_memory": {
          "type": "integer",
          "enum": [
            8,
            16,
            32
          ],
          "example": 16
        },
        "language": {
          "type": "string"
        },
        "do_not_track": {
          "type": "boolean"
        }
      }
    },
    "screen": {
      "type": "object",
      "additionalProperties": true,
      "properties": {
        "width": {
          "type": "integer"
        },
        "height": {
          "type": "integer"
        },
        "avail_width": {
          "type": "integer"
        },
        "avail_height": {
          "type": "integer"
        },
        "color_depth": {
          "type": "integer",
          "example": 24
        },
        "device_pixel_ratio": {
          "type": "number",
          "example": 2
        },
        "color_gamut": {
          "type": "string",
          "enum": [
            "",
            "srgb",
            "p3",
            "rec2020"
          ]
        },
        "refresh_rate": {
          "type": "integer",
          "minimum": 24,
          "maximum": 480,
          "example": 60,
          "description": "How often the claimed display refreshes. Absent means 60. See the refresh_rate field on the create/edit requests."
        }
      }
    },
    "client_hints": {
      "type": "object",
      "description": "Sec-CH-UA family (browser.vision calls this `hints`).",
      "additionalProperties": true,
      "properties": {
        "platform": {
          "type": "string"
        },
        "platform_version": {
          "type": "string"
        },
        "architecture": {
          "type": "string",
          "example": "x86"
        },
        "bitness": {
          "type": "string",
          "example": "64"
        },
        "mobile": {
          "type": "boolean"
        }
      }
    },
    "webgl": {
      "type": "object",
      "additionalProperties": true,
      "properties": {
        "unmasked_vendor": {
          "type": "string"
        },
        "unmasked_renderer": {
          "type": "string"
        }
      }
    },
    "webgpu": {
      "type": "object",
      "additionalProperties": true
    },
    "webrtc": {
      "type": "string",
      "description": "WebRTC handling mode.",
      "enum": [
        "auto",
        "tcp_only",
        "block"
      ]
    },
    "noise": {
      "$ref": "#/components/schemas/NoisePrefs"
    },
    "network": {
      "type": "object",
      "properties": {
        "blocked_ports": {
          "type": "array",
          "items": {
            "type": "integer"
          },
          "description": "Local-network port-scan defense (ports_protection)."
        }
      }
    },
    "timezone": {
      "type": "string",
      "description": "IANA name, or \"auto\" to resolve from the bound proxy at launch."
    }
  }
}
```

## NoisePrefs

```json
{
  "type": "object",
  "description": "Per-vector anti-fingerprint noise. Each block is `{enabled, seed}` (+ feature-specific knobs). `enabled:false` = pass-through.",
  "properties": {
    "canvas": {
      "$ref": "#/components/schemas/NoiseBlock"
    },
    "webgl": {
      "allOf": [
        {
          "$ref": "#/components/schemas/NoiseBlock"
        }
      ],
      "properties": {
        "intensity": {
          "type": "number"
        }
      }
    },
    "audio": {
      "allOf": [
        {
          "$ref": "#/components/schemas/NoiseBlock"
        }
      ],
      "properties": {
        "intensity": {
          "type": "number"
        }
      }
    },
    "client_rects": {
      "allOf": [
        {
          "$ref": "#/components/schemas/NoiseBlock"
        }
      ],
      "properties": {
        "max_offset": {
          "type": "number"
        }
      }
    },
    "sensors": {
      "$ref": "#/components/schemas/NoiseBlock"
    },
    "fonts": {
      "allOf": [
        {
          "$ref": "#/components/schemas/NoiseBlock"
        }
      ],
      "description": "`enabled:true` hides a small deterministic per-profile subset of non-essential fonts (real host fonts otherwise pass through)."
    }
  }
}
```

## NoiseBlock

```json
{
  "type": "object",
  "properties": {
    "enabled": {
      "type": "boolean"
    },
    "seed": {
      "type": "integer",
      "description": "Per-profile entropy; 0 = derive automatically."
    }
  }
}
```

## CreateProfileRequest

```json
{
  "type": "object",
  "required": [
    "fingerprint"
  ],
  "properties": {
    "name": {
      "type": "string"
    },
    "notes": {
      "type": "string"
    },
    "proxy_id": {
      "type": "string",
      "description": "Bind a proxy already in the store (by id)."
    },
    "proxy": {
      "type": "string",
      "description": "Attach a proxy by string (`host:port:user:pass` or `scheme://user:pass@host:port`). On a **persistent** profile it is added to the proxy store (deduped by endpoint) and full-tested (UDP + geo), then bound by id; so it appears in the proxy list and QUIC/WebRTC gating has a cached result. On a **temporary** profile it is attached inline (not stored). Ignored if `proxy_id` is set.",
      "example": "socks5://user:pass@1.2.3.4:1080"
    },
    "folder": {
      "type": "string",
      "description": "Folder tag (ignored on /folders/{folder}/profiles, which wins)."
    },
    "color": {
      "type": "string",
      "description": "Accent for the profile's window icon and for the pill carrying its name in the omnibox. Omit it and the icon derives a colour from the name while the pill keeps the browser's own toolbar colour.",
      "example": "#2FCB80"
    },
    "extensions": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Extension-library ids (`GET /extensions`), loaded at launch. An id the library does not hold is a 400 rather than a profile that silently starts without it."
    },
    "refresh_rate": {
      "type": "integer",
      "minimum": 24,
      "maximum": 480,
      "example": 60,
      "description": "Claimed display refresh rate, written into `screen.refresh_rate`. No web API reports it; a page measures it by timing requestAnimationFrame, which is why leaving it out is not neutral; absent means 60, what most machines report, rather than whatever the host's own screen does. Frames can only be slowed, so the effective rate is the closest whole division of the host's panel at or under this."
    },
    "fingerprint": {
      "allOf": [
        {
          "$ref": "#/components/schemas/Fingerprint"
        }
      ],
      "description": "Stored verbatim. Get one from /fingerprint/new and edit as needed."
    }
  }
}
```

## EditProfileRequest

```json
{
  "type": "object",
  "description": "All fields optional; only what's present is changed.",
  "properties": {
    "name": {
      "type": "string"
    },
    "notes": {
      "type": "string"
    },
    "folder": {
      "type": "string",
      "description": "\"\" unfiles (moves to All)."
    },
    "proxy_id": {
      "type": "string",
      "description": "\"\" unbinds the proxy."
    },
    "proxy": {
      "type": "string",
      "description": "Proxy string; stored (deduped) + full-tested, then bound."
    },
    "color": {
      "type": "string",
      "description": "`#rrggbb`; \"\" goes back to the name-derived colour."
    },
    "extensions": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Replaces the whole list; `[]` loads none."
    },
    "refresh_rate": {
      "type": "integer",
      "minimum": 24,
      "maximum": 480,
      "example": 60,
      "description": "Claimed display refresh rate, applied AFTER `fingerprint` and written into `screen.refresh_rate`. No web API reports it; a page measures it by timing requestAnimationFrame, which is why leaving it out is not neutral; absent means 60, what most machines report, rather than whatever the host's own screen does. Frames can only be slowed, so the effective rate is the closest whole division of the host's panel at or under this."
    },
    "fingerprint": {
      "allOf": [
        {
          "$ref": "#/components/schemas/Fingerprint"
        }
      ],
      "description": "When present, REPLACES the stored fingerprint verbatim."
    }
  }
}
```

## TempProfileRequest

```json
{
  "type": "object",
  "properties": {
    "fingerprint_id": {
      "type": "string",
      "description": "Library entry to base off; random match for `platform` when omitted."
    },
    "platform": {
      "type": "string",
      "enum": [
        "Windows",
        "macOS",
        "Linux"
      ],
      "description": "Used to pick a random fingerprint when `fingerprint_id` is omitted; host OS when both omitted."
    },
    "proxy": {
      "type": "string",
      "description": "Inline proxy string (not saved to the store)."
    },
    "name": {
      "type": "string"
    },
    "folder": {
      "type": "string"
    },
    "refresh_rate": {
      "type": "integer",
      "minimum": 24,
      "maximum": 480,
      "example": 60,
      "description": "Claimed display refresh rate, written into `screen.refresh_rate`. No web API reports it; a page measures it by timing requestAnimationFrame, which is why leaving it out is not neutral; absent means 60, what most machines report, rather than whatever the host's own screen does. Frames can only be slowed, so the effective rate is the closest whole division of the host's panel at or under this."
    },
    "noise": {
      "type": "object",
      "description": "Per-vector fingerprint noise. Omitted vectors keep their defaults (disabled). Two formats can be combined: shorthand `{\"canvas\": true, \"webgl\": true}`, which also applies the UI default intensity to `webgl` and `client_rects`, and a full object such as `{\"canvas\": {\"enabled\": true, \"seed\": 42}}`, matching the format returned by `GET /profiles/{id}`.\n\nFields are merged with the existing vector settings. A missing or zero `seed` generates a stable value from the profile ID.",
      "additionalProperties": {
        "oneOf": [
          {
            "type": "boolean"
          },
          {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean"
              },
              "seed": {
                "type": "integer",
                "format": "int64"
              },
              "intensity": {
                "type": "number",
                "description": "`webgl` only."
              },
              "max_offset": {
                "type": "number",
                "description": "`client_rects` only."
              }
            }
          }
        ]
      },
      "properties": {
        "canvas": {},
        "webgl": {},
        "audio": {},
        "client_rects": {},
        "sensors": {},
        "fonts": {}
      },
      "example": {
        "canvas": true,
        "webgl": {
          "enabled": true,
          "intensity": 0.0005
        },
        "fonts": true
      }
    }
  }
}
```

## Extension

```json
{
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "description": "Pass this in a profile's `extensions`."
    },
    "name": {
      "type": "string"
    },
    "version": {
      "type": "string"
    },
    "description": {
      "type": "string"
    },
    "size_bytes": {
      "type": "integer",
      "format": "int64"
    },
    "added_at": {
      "type": "string",
      "description": "`@<unix_secs>`."
    },
    "icon": {
      "type": "string",
      "nullable": true,
      "description": "Largest icon the manifest declares, as a `data:` URL. Null on the listing endpoint and on extensions that ship none."
    }
  }
}
```

## AddExtensionRequest

```json
{
  "type": "object",
  "description": "Exactly one of `url` or `path`.",
  "properties": {
    "url": {
      "type": "string",
      "description": "A Chrome Web Store page, a bare 32-character extension id, or a direct link to a `.crx` / `.zip`.",
      "example": "https://chromewebstore.google.com/detail/rabby-wallet/acmacodkjbdgmoleebolmdjonilkdbch"
    },
    "path": {
      "type": "string",
      "description": "A local `.crx`, `.zip` or unpacked folder on the launcher's machine."
    }
  }
}
```

## Bookmark

```json
{
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "format": "uuid"
    },
    "title": {
      "type": "string"
    },
    "url": {
      "type": "string"
    },
    "folder": {
      "type": "string",
      "description": "Launcher folder; empty means every profile."
    }
  }
}
```

## BookmarkRequest

```json
{
  "type": "object",
  "required": [
    "url"
  ],
  "properties": {
    "id": {
      "type": "string",
      "description": "Present updates that bookmark; absent creates one."
    },
    "url": {
      "type": "string",
      "description": "A bare host is fine; `https://` is added.",
      "example": "facebook.com/ads/manager"
    },
    "title": {
      "type": "string",
      "description": "Blank uses the address itself."
    },
    "folder": {
      "type": "string",
      "description": "Empty (or absent) means every profile."
    }
  }
}
```

## TrashEntry

```json
{
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "format": "uuid"
    },
    "name": {
      "type": "string"
    },
    "folder": {
      "type": "string"
    },
    "deleted_at": {
      "type": "integer",
      "format": "int64",
      "description": "Unix seconds."
    },
    "expires_at": {
      "type": "integer",
      "format": "int64",
      "description": "Unix seconds; the startup sweep removes anything past it."
    },
    "size_bytes": {
      "type": "integer",
      "format": "int64",
      "description": "The archive, which holds the account and not the caches."
    }
  }
}
```

## ProfileMeta

```json
{
  "type": "object",
  "description": "Launcher-facing profile metadata (returned by create).",
  "properties": {
    "id": {
      "type": "string",
      "format": "uuid"
    },
    "name": {
      "type": "string"
    },
    "notes": {
      "type": "string"
    },
    "proxy_id": {
      "type": "string",
      "nullable": true
    },
    "last_launched_at": {
      "type": "string",
      "nullable": true,
      "description": "ISO-8601 or null."
    },
    "created_at": {
      "type": "string",
      "nullable": true
    },
    "pinned": {
      "type": "boolean"
    },
    "folder": {
      "type": "string"
    },
    "color": {
      "type": "string",
      "nullable": true,
      "description": "Icon accent as `#rrggbb`; `null` means it is derived from the name, which is what the browser does when nothing is chosen."
    },
    "extensions": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Extension-library ids loaded at launch."
    }
  }
}
```

## ProfileListItem

```json
{
  "allOf": [
    {
      "$ref": "#/components/schemas/ProfileMeta"
    },
    {
      "type": "object",
      "properties": {
        "running": {
          "type": "boolean"
        },
        "pid": {
          "type": "integer",
          "format": "int64",
          "nullable": true
        },
        "cdp": {
          "nullable": true,
          "allOf": [
            {
              "$ref": "#/components/schemas/CdpInfo"
            }
          ]
        }
      }
    }
  ]
}
```

## StoredProfile

```json
{
  "type": "object",
  "description": "Full on-disk profile: the `_meta` envelope plus the flattened FingerprintConfig (all `Fingerprint` fields appear at the top level). `running`/`cdp` are present only while the profile runs.",
  "additionalProperties": true,
  "properties": {
    "_meta": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "format": "uuid"
        },
        "proxy_id": {
          "type": "string",
          "nullable": true
        },
        "inline_proxy": {
          "nullable": true,
          "allOf": [
            {
              "$ref": "#/components/schemas/ProxyEntry"
            }
          ]
        },
        "last_launched_at": {
          "type": "string",
          "nullable": true
        },
        "created_at": {
          "type": "string",
          "nullable": true
        },
        "pinned": {
          "type": "boolean"
        },
        "folder": {
          "type": "string"
        },
        "temporary": {
          "type": "boolean"
        },
        "rev": {
          "type": "integer",
          "description": "Bumped by every write. The desktop editor sends back the number it opened the profile at, so a save that would land on top of a change made through this API is refused rather than silently reverting it. API edits are never refused; they read the profile and write it back in one step."
        }
      }
    },
    "running": {
      "type": "boolean"
    },
    "cdp": {
      "nullable": true,
      "allOf": [
        {
          "$ref": "#/components/schemas/CdpInfo"
        }
      ]
    }
  }
}
```

## CdpInfo

```json
{
  "type": "object",
  "description": "Chrome DevTools Protocol endpoint of a running profile.",
  "properties": {
    "port": {
      "type": "integer",
      "description": "Chrome-chosen remote-debugging port."
    },
    "http_url": {
      "type": "string",
      "example": "http://127.0.0.1:53217",
      "description": "REST base (/json, /json/version)."
    },
    "web_socket_debugger_url": {
      "type": "string",
      "example": "ws://127.0.0.1:53217/devtools/browser/ab12-...",
      "description": "Browser-level WS endpoint for Puppeteer/Playwright `connect`."
    }
  }
}
```

## RunningProfile

```json
{
  "type": "object",
  "properties": {
    "profile_id": {
      "type": "string"
    },
    "pid": {
      "type": "integer",
      "format": "int64"
    },
    "cdp": {
      "nullable": true,
      "allOf": [
        {
          "$ref": "#/components/schemas/CdpInfo"
        }
      ]
    }
  }
}
```

## LibraryEntry

```json
{
  "type": "object",
  "properties": {
    "id": {
      "type": "string"
    },
    "label": {
      "type": "string"
    },
    "platform": {
      "type": "string",
      "enum": [
        "Windows",
        "macOS",
        "Linux"
      ]
    },
    "chrome": {
      "type": "string",
      "description": "Chrome version string."
    },
    "gpu": {
      "type": "string"
    },
    "builtin": {
      "type": "boolean",
      "description": "Part of the bundled starter set."
    }
  }
}
```

## AddProxyRequest

```json
{
  "type": "object",
  "description": "A proxy string OR explicit fields (string wins).",
  "properties": {
    "proxy": {
      "type": "string",
      "description": "`scheme://user:pass@host:port` or `host:port:user:pass`."
    },
    "kind": {
      "type": "string",
      "enum": [
        "socks5",
        "http",
        "https"
      ]
    },
    "host": {
      "type": "string"
    },
    "port": {
      "type": "integer"
    },
    "username": {
      "type": "string"
    },
    "password": {
      "type": "string"
    },
    "name": {
      "type": "string"
    },
    "country": {
      "type": "string"
    },
    "notes": {
      "type": "string"
    }
  }
}
```

## ProxySummary

```json
{
  "type": "object",
  "description": "Stored proxy without credentials.",
  "properties": {
    "id": {
      "type": "string"
    },
    "name": {
      "type": "string"
    },
    "kind": {
      "type": "string",
      "enum": [
        "socks5",
        "http",
        "https"
      ]
    },
    "host": {
      "type": "string"
    },
    "port": {
      "type": "integer"
    },
    "country": {
      "type": "string",
      "description": "Informational tag (e.g. \"US\")."
    }
  }
}
```

## Cookie

```json
{
  "type": "object",
  "description": "A cookie in a tool-friendly shape (compatible with EditThisCookie / Puppeteer exports; `httpOnly`/`sameSite` are accepted as aliases on import).",
  "required": [
    "domain",
    "name",
    "value"
  ],
  "properties": {
    "domain": {
      "type": "string",
      "description": "Cookie host (Chromium `host_key`), e.g. \".example.com\"."
    },
    "name": {
      "type": "string"
    },
    "value": {
      "type": "string"
    },
    "path": {
      "type": "string",
      "default": "/"
    },
    "expires": {
      "type": "number",
      "nullable": true,
      "description": "Unix seconds; null/absent = session cookie."
    },
    "secure": {
      "type": "boolean",
      "default": false
    },
    "http_only": {
      "type": "boolean",
      "default": false
    },
    "same_site": {
      "type": "string",
      "enum": [
        "Strict",
        "Lax",
        "None",
        "unspecified"
      ],
      "description": "Case-insensitive on import."
    }
  }
}
```

## ProxyEntry

```json
{
  "type": "object",
  "description": "Full proxy (as embedded in `_meta.inline_proxy`).",
  "properties": {
    "id": {
      "type": "string"
    },
    "name": {
      "type": "string"
    },
    "kind": {
      "type": "string",
      "enum": [
        "socks5",
        "http",
        "https"
      ]
    },
    "host": {
      "type": "string"
    },
    "port": {
      "type": "integer"
    },
    "username": {
      "type": "string"
    },
    "password": {
      "type": "string"
    },
    "country": {
      "type": "string"
    },
    "notes": {
      "type": "string"
    }
  }
}
```

## AutomationBranch

```json
{
  "description": "Where the run goes after a step. A plain string for the simple cases, an object for the ones carrying a value.",
  "oneOf": [
    {
      "type": "string",
      "enum": [
        "next",
        "stop",
        "endpass"
      ],
      "description": "`next`; the step below. `stop`; end this profile's whole run. `endpass`; end this pass; the next one starts from the top."
    },
    {
      "type": "object",
      "properties": {
        "goto": {
          "type": "string",
          "description": "Block id to jump to; one that no longer exists behaves as next."
        }
      }
    },
    {
      "type": "object",
      "properties": {
        "retry": {
          "type": "integer",
          "description": "Try this same step again, that many more times, then take next."
        }
      }
    }
  ]
}
```

## AutomationBlock

```json
{
  "type": "object",
  "description": "One step. `kind` and `params` are open; a module can add a kind this build has never heard of.",
  "properties": {
    "id": {
      "type": "string"
    },
    "kind": {
      "type": "string",
      "description": "A built-in palette kind (`goto`, `click`, `type`, `profile.create`, `var.set`, ...) or `module:<module id>:<block>`.\n\nOn a mobile profile, common actions use touch input: `click` taps, `rightClick` performs a long press, and `scroll` and `swipe` use touch gestures. `hover` is unavailable because mobile devices have no pointer. The following mobile-only steps are rejected on desktop profiles:\n\n| kind | params | action |\n| --- | --- | --- |\n| `touch.tap` | `selector` or `x`+`y`, `tapCount` (1-3), `timeout` | Tap the target. |\n| `touch.longPress` | `selector` or `x`+`y`, `timeout` | Press and hold. |\n| `touch.swipe` | `selector` or `x`+`y`, `dx`, `dy`, `timeout` | Swipe. |\n| `touch.drag` | `selector`, `to`, `holdMs`, `timeout` | Drag an element. |\n| `touch.pinch` | `scale`, `selector` or `x`+`y` | Pinch to zoom. Values above 1 zoom out. |\n| `screen.rotate` | `angle` (0/90/180/270), `turnMs`, `into` | Rotate the device. `into` stores the new `screen.orientation.type`. |",
      "example": "click"
    },
    "label": {
      "type": "string"
    },
    "params": {
      "type": "object",
      "additionalProperties": true,
      "description": "The step's own fields, as the palette defines them."
    },
    "enabled": {
      "type": "boolean",
      "default": true
    },
    "x": {
      "type": "number",
      "description": "Canvas position. Layout only; the run follows the branches."
    },
    "y": {
      "type": "number"
    },
    "on_done": {
      "$ref": "#/components/schemas/AutomationBranch"
    },
    "on_fail": {
      "allOf": [
        {
          "$ref": "#/components/schemas/AutomationBranch"
        }
      ],
      "description": "Defaults to `stop`; carrying on after a click that found nothing is how a run types into the wrong page."
    },
    "secrets": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Parameter names emptied on export."
    }
  }
}
```

## AutomationRunSettings

```json
{
  "type": "object",
  "properties": {
    "threads": {
      "type": "integer",
      "default": 1,
      "description": "Browsers running at once; clamped to 1-64."
    },
    "loops": {
      "type": "integer",
      "default": 1,
      "description": "Passes over the blocks. 0 means run until `hours` is up."
    },
    "hours": {
      "type": "number",
      "description": "Only meaningful when `loops` is 0."
    },
    "start": {
      "type": "string",
      "description": "Block id to start at; empty means the first."
    },
    "profiles": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Kept for projects saved before profiles became blocks; one thread each when set."
    }
  }
}
```

## AutomationProject

```json
{
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "format": "uuid"
    },
    "name": {
      "type": "string"
    },
    "notes": {
      "type": "string"
    },
    "blocks": {
      "type": "array",
      "items": {
        "$ref": "#/components/schemas/AutomationBlock"
      }
    },
    "run": {
      "$ref": "#/components/schemas/AutomationRunSettings"
    },
    "created_at": {
      "type": "integer",
      "description": "Unix seconds."
    },
    "updated_at": {
      "type": "integer",
      "description": "Unix seconds."
    }
  }
}
```

## AutomationBundle

```json
{
  "type": "object",
  "description": "A project on its way to another machine.",
  "properties": {
    "format": {
      "type": "integer",
      "example": 1,
      "description": "An import refuses a number it does not know."
    },
    "exported_at": {
      "type": "integer",
      "description": "Unix seconds."
    },
    "project": {
      "$ref": "#/components/schemas/AutomationProject"
    },
    "modules": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Module ids the steps need."
    },
    "module_files": {
      "type": "array",
      "description": "Those modules' files, carried inside the bundle so an imported project can run where it lands. A module missing from disk at export time is left out here but still named in `modules`. On import, a module already installed under the same id is kept as it is rather than overwritten.",
      "items": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "wasm": {
            "type": "string",
            "description": "The .wasm, gzipped then base64."
          }
        }
      }
    },
    "needs": {
      "type": "array",
      "description": "What export blanked, so an import can ask for exactly that.",
      "items": {
        "type": "object",
        "properties": {
          "block_id": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "params": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      }
    }
  }
}
```

## AutomationWorker

```json
{
  "type": "object",
  "description": "One browser inside a run.",
  "properties": {
    "profile_id": {
      "type": "string"
    },
    "profile_name": {
      "type": "string"
    },
    "pass": {
      "type": "integer",
      "description": "1-based; 0 before the first pass starts."
    },
    "step": {
      "type": "integer"
    },
    "steps_total": {
      "type": "integer"
    },
    "status": {
      "type": "string",
      "enum": [
        "queued",
        "starting",
        "running",
        "done",
        "failed",
        "stopped"
      ]
    },
    "note": {
      "type": "string"
    }
  }
}
```

## AutomationRun

```json
{
  "type": "object",
  "properties": {
    "project_id": {
      "type": "string",
      "format": "uuid"
    },
    "project_name": {
      "type": "string"
    },
    "started_at": {
      "type": "integer",
      "description": "Unix seconds."
    },
    "running": {
      "type": "boolean"
    },
    "workers": {
      "type": "array",
      "items": {
        "$ref": "#/components/schemas/AutomationWorker"
      }
    },
    "log": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "The tail the fleet window shows; the whole log is on disk under `automation-runs/`."
    }
  }
}
```

## AutomationModule

```json
{
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "description": "File stem; also the prefix every one of its block kinds carries."
    },
    "name": {
      "type": "string"
    },
    "path": {
      "type": "string"
    },
    "blocks": {
      "type": "array",
      "items": {
        "type": "object",
        "additionalProperties": true
      },
      "description": "Block descriptors, in the same shape as the built-in palette."
    },
    "error": {
      "type": "string",
      "description": "Empty when the module loaded; otherwise why it did not."
    }
  }
}
```
