HIPsHanzo Proposals
Back to HIPs
HIP-1040FinalStandards TrackInterface

Appearance

Hanzo AI
Created: 2026-08-20
Requires: HIP-0026

HIP-1040: Appearance

Abstract

/v1/account/appearance is one person's reading of the design system — text size, spacing density, one accent hue — held on their IAM account rather than in a browser. It is a facet of the account capability (HIP-1200), served by apps/account in hanzoai/cloud (apps/account/appearance.go); the router still serves it at the bare root today, a pair hanzoai/cloud openapi/misfiled.txt carries.

The whole claim is where the value lives. A preference kept in local storage is a fact about a device; kept on the identity it is a fact about the person, so it is the same in the console, in chat, on the desktop and on a phone that has never seen any of them.

Motivation

Every browser surface that could set this is either a static export or a separate frontend, so none of them holds the confidential IAM credential needed to write a user row. Each one growing its own writer would mean several read-merge-write implementations against one row, and a row whose password hash a partial write can blank. There is one writer, at the unified API host, acting as the confidential console client against the already-validated caller's own record.

Specification

The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as in RFC 2119.

1. Subject

The subject is the validated caller and MUST NOT be nameable. There is no parameter for whose appearance is read or written; an unauthenticated request is refused rather than answered with a default (apps/account/appearance.go:83, :106).

2. Storage

The preference is one JSON value in the IAM user row's properties map. Gaining it changes no IAM schema.

A write MUST read the full row, change that one property, and submit the whole row back (apps/account/appearance.go:151). IAM's update overwrites the default column set, so a partial submission blanks every field the body did not restate — including the password hash. A write that cannot first read its row MUST fail rather than write a row it did not read.

3. Axes, and absence

Three axes, each OPTIONAL:

axisdomain
typetext-size multiplier, clamped to [0.85, 1.4]
densitycompact, default or comfortable
accentone CSS colour token

An unset axis is ABSENT and MUST NOT be stored as a neutral value. Absence and "the published default" are the same state, so an empty preference cannot stamp a scale over a brand that set its own.

A value outside its domain is DROPPED and the rest of the preference is stored: one bad axis narrows the result, it never refuses the write (cleanAppearance, apps/account/appearance.go:61).

4. The accent is validated as a colour

accent is rendered by surfaces into a <style> body, and its value is chosen by its owner. It MUST therefore match a closed colour grammar before it is stored — a hex literal, or a bounded functional-colour form — and anything carrying ;, {, }, < or url( cannot match (apps/account/appearance.go:56). Validation is at the WRITE, so no reader has to be trusted to sanitize.

5. The read fails soft

A transient identity-service failure on READ answers the empty preference, not a 5xx (apps/account/appearance.go:83). A surface then applies its published default, which is what it would have done for a person who never set one. A WRITE does not fail soft: an unstored preference MUST be reported as unstored.

Rationale

The obvious alternative is a per-application setting, filed with the rest of an app's preferences. It is wrong by counting: there is one of these per identity and not one per app, and a person who enlarges text does not mean "in this tab".

Reusing the IAM properties map rather than adding a column costs one JSON decode per read and buys a preference that ships without a migration.

Security Considerations

The stored accent reaches a stylesheet. Treating it as opaque text and validating it only where it is rendered would put the check in as many places as there are surfaces, and a new surface would default to unsafe; the grammar at the write is the one place that cannot be skipped.

The whole-row re-submit is a footgun that has to be stated: the row carries credential material, so the write path's correctness is the row's integrity. The same read-merge-write serves the avatar (HIP-1042), and both MUST refuse to write a row they could not read.

References

  • HIP-0026 — Identity & Access Management Standard
  • HIP-0106 — The Hanzo Plugin Contract
  • HIP-1042 — Avatar
  • HIP-1200 — Account — The Caller's Own Surface

Copyright

Released under CC0 1.0 Universal Public Domain Dedication.