# UI Skins Reference

UI skins are local ZIP packages that customize Construct's dashboard with
validated design tokens and local image assets. v1 skins are intentionally not
plugins: they cannot run JavaScript, ship arbitrary CSS, embed HTML, use SVG, or
load remote URLs.

## ZIP Structure

Each package must include `construct-skin.json` at the ZIP root.

```text
construct-skin.json
assets/
  logo.png
  avatar.webp
  hero.jpg
  shell-texture.png
  panel-decoration.webp
```

Only these file extensions are accepted:

- `.json`
- `.png`
- `.jpg`
- `.jpeg`
- `.webp`

Assets must live under `assets/`. The gateway stores imported skins under:

```text
{workspace_dir}/skins/{skin_id}/manifest.json
{workspace_dir}/skins/{skin_id}/assets/...
```

## Manifest Schema

```json
{
  "schemaVersion": 1,
  "id": "rabbit_garden",
  "name": "Rabbit Garden",
  "version": "1.0.0",
  "modes": {
    "light": {
      "tokens": {
        "--construct-bg-base": "#f7fbf2",
        "--construct-bg-surface": "#ffffff",
        "--construct-text-primary": "#142018",
        "--construct-signal-live": "#4e9f5b",
        "--construct-radius-md": "14px"
      },
      "assets": {
        "brandLogo": "assets/logo.png",
        "operatorAvatar": "assets/avatar.webp",
        "dashboardHero": "assets/hero.jpg",
        "dashboardShowcase": "assets/showcase.webp",
        "shellTexture": "assets/shell-texture.png",
        "panelDecoration": "assets/panel-decoration.webp",
        "riskRailDecoration": "assets/risk-rail.webp",
        "statusSuccessBadge": "assets/success-badge.webp"
      },
      "preview": "assets/hero.jpg"
    },
    "dark": {
      "tokens": {
        "--construct-bg-base": "#090607",
        "--construct-bg-surface": "#151012",
        "--construct-text-primary": "#f6ecef",
        "--construct-signal-live": "#f08aaa"
      },
      "assets": {
        "brandLogo": "assets/logo.png",
        "operatorAvatar": "assets/avatar.webp"
      },
      "preview": "assets/hero.jpg"
    }
  }
}
```

Required manifest fields:

| Field | Purpose |
|---|---|
| `schemaVersion` | Must be `1` |
| `id` | Storage ID; lowercase letters, numbers, `-`, and `_` only |
| `name` | Display name |
| `version` | Skin package version |
| `modes` | Object containing `light`, `dark`, or both |

Supported modes are `modes.light` and `modes.dark`. Each mode may define
`tokens`, `assets`, and `preview`.

## Token Contract

Skin-authored tokens must use the canonical `--construct-*` namespace.
Existing `--pc-*` compatibility tokens are generated by the frontend bridge and
must not be authored directly.

Accepted token values:

- color values: hex, `rgb()`, `rgba()`, `hsl()`, `hsla()`
- bounded CSS lengths for radius, spacing, and font-size-like tokens: `px`,
  `rem`, `em`, or `%`

Rejected in v1:

- `url(...)`
- `var(...)`
- `expression(...)`
- JavaScript-like URLs
- shadow tokens
- background-image tokens
- arbitrary CSS snippets

Common useful tokens:

| Token | Use |
|---|---|
| `--construct-bg-base` | page background |
| `--construct-bg-shell` | sidebar/shell surface |
| `--construct-bg-surface` | cards and contained surfaces |
| `--construct-bg-panel` | translucent panel background |
| `--construct-border-soft` | low-emphasis borders |
| `--construct-border-strong` | active/selected borders |
| `--construct-text-primary` | primary text |
| `--construct-text-secondary` | secondary text |
| `--construct-text-muted` | muted labels |
| `--construct-signal-live` | primary accent |
| `--construct-signal-network` | secondary accent |
| `--construct-radius-sm` | compact radius |
| `--construct-radius-md` | panel radius |
| `--construct-radius-lg` | large radius |

## Asset Slots

| Slot | UI use |
|---|---|
| `brandLogo` | sidebar/header brand image |
| `operatorAvatar` | operator/agent avatar surfaces |
| `dashboardHero` | dashboard banner image |
| `shellTexture` | subtle shell background texture |
| `panelDecoration` | subtle panel corner decoration |
| `pageBackdrop` | full application backdrop behind the shell |
| `sidebarBackdrop` | sidebar-specific background art or texture |
| `headerBackdrop` | top header command-band background art or texture |
| `dashboardShowcase` | large dashboard visual panel for character, room, product, or scene art |
| `dashboardAccent` | small dashboard showcase accent prop or emblem |
| `graphBackdrop` | workflow/team graph canvas background |
| `metricDecoration` | overview metric and mini-metric card decoration |
| `runCardDecoration` | selected-run and run-summary decoration |
| `stepCardDecoration` | selected-step/selected-node decoration |
| `timelineDecoration` | priority timeline decoration |
| `riskRailDecoration` | risk rail card decoration |
| `agentRailDecoration` | agent rail card decoration |
| `commandBandDecoration` | command band card decoration |
| `recentRunsDecoration` | recent runs rail decoration |
| `statusRunningBadge` | running status badge art |
| `statusSuccessBadge` | completed/success status badge art |
| `statusFailedBadge` | failed status badge art |
| `statusPendingBadge` | pending/blocked status badge art |
| `statusSkippedBadge` | skipped status badge art |

Asset values must be relative paths under `assets/`, for example
`assets/logo.webp`. Remote URLs, absolute paths, path traversal, and SVG are
rejected.

## Profile Images

Agent and team profile images are intentionally not UI skin asset slots. Skin
assets are theme-scoped visuals that ship in a ZIP package and apply globally.
Per-agent and per-team profile images are identity assets, so they are uploaded
from the Agent Pool and Agent Teams screens and stored as Kumiho file artifacts.

Supported profile image uploads:

| Target | Upload route | Artifact name | Storage shape |
|---|---|---|---|
| Agent | `POST /api/agents/avatar` | `profile-avatar` | `{workspace_dir}/artifacts/{project}/AgentPool/{agent}/avatars/{uuid}.{ext}` |
| Team | `POST /api/teams/avatar` | `profile-avatar` | `{workspace_dir}/artifacts/{project}/Teams/{team}/avatars/{uuid}.{ext}` |

Profile uploads accept PNG, JPEG, and WebP images up to 5 MiB. SVG and other
file types are rejected, even when the file extension is allowed. Uploads are
multipart requests with `kref` and `file` fields.

Uploading a profile image creates a new published revision for the agent or
team and stores the artifact metadata on the revision:

```json
{
  "avatar_artifact_name": "profile-avatar",
  "avatar_filename": "reviewer.png",
  "avatar_mime": "image/png",
  "avatar_location": "{workspace_dir}/artifacts/{project}/AgentPool/reviewer/avatars/{uuid}.png"
}
```

The gateway returns a signed local workspace URL as `avatar_url`. Editing an
agent or team without uploading a new image preserves the existing avatar
metadata. Uploading a replacement image writes a new artifact file and updates
the revision metadata.

Workflow and topology surfaces render these identity assets separately from the
skin:

| Surface | Profile image behavior |
|---|---|
| Workflow task nodes | Assigned agent avatars are shown as a large node background layer and as a compact identity chip. Resolution prefers `runInfo.template_name`, then `assign`, then `template`. |
| Run DAG nodes | Runtime step nodes use the same assigned-agent avatar treatment when the agent can be resolved. |
| Agent Teams | Team cards and headers show the team avatar. Team topology member nodes show the member agent avatar as a large background layer and compact identity chip. |

## Gateway API

Routes:

| Route | Purpose |
|---|---|
| `GET /api/skins` | list installed skins |
| `POST /api/skins/import` | import a ZIP package |
| `DELETE /api/skins/{id}` | delete an installed skin |
| `GET /api/skins/{id}/assets/{path}` | serve a stored skin asset |

Mutation routes require normal gateway authentication. The import route has its
own upload limit and timeout:

| Limit | Value |
|---|---|
| ZIP body | 25 MiB |
| Request timeout | 120 seconds |
| Extracted total size | 50 MiB |
| File count | 128 |

Asset serving returns a content type, `X-Content-Type-Options: nosniff`, and
private cache headers.

## Validation Rules

The importer rejects packages that contain:

- missing or duplicate root `construct-skin.json`
- absolute paths
- `..` path traversal
- path separators that escape the skin root
- symlinks
- duplicate ZIP entries
- unsupported extensions
- unsupported modes or asset slots
- invalid skin IDs
- manifest asset references that are missing from the ZIP
- JavaScript, arbitrary CSS, HTML, SVG, remote URLs, or executable behavior

Deleting a skin removes only `{workspace_dir}/skins/{skin_id}` after canonical
path verification.
