Files
hips/HIPs/hip-1141-pref-personal-settings.md
zeekayandHanzo Dev 7a9dcac628 What each runtime implements of the Hanzo corpus
An audit read the code and found the proposal that specifies it. Go reaches
all 122 capability proposals and 64 of the 121 other Standards Track ones;
Rust has 35, mostly the operator and the CLI; C++ has three, because the
complete original C++ in the estate is a ZAP handler, a bot nothing specifies,
and authored kernels inside forked CUDA and Metal crates.

    | | shipped | partial | none | not assessed |
    | Go   | 146 | 33 | 1 |  80 |
    | C++  |   1 |  2 | 0 | 257 |
    | Rust |  10 | 25 | 1 | 224 |

The large "not assessed" columns are the point rather than a gap. The model and
training specs are Python, the frontend and design-system proposals are
TypeScript, and recording `none` against those would read as a search that
came up empty when the honest statement is that the runtime was never the
question. Governance, Meta and Process are not graded at all.

Six proposals are Final with nothing found in Go and are left unwritten rather
than demoted: 0065, 0089, 0098, 0103, 0521 and 0902. HIP-0521 answers itself,
its own section reading "Not yet implemented". Three of the others name a
canonical Go file in consensus/protocol/auth that nobody wrote, in a package
where the four neighbouring proposals' files all exist with tests beside them.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-05 19:18:13 -07:00

5.0 KiB

hip, title, author, type, category, capability, status, implementation-go, created, requires
hip title author type category capability status implementation-go created requires
1141 Pref — One Document Per Person Hanzo AI Standards Track Interface pref Final shipped 2026-08-20 HIP-0026, HIP-0106, HIP-0139

HIP-1141: Pref — One Document Per Person

Abstract

/v1/pref is the signed-in user's own UI state — theme, density, pinned nav — one JSON document per person, following them across every Hanzo surface. It is implemented in hanzoai/cloud at apps/pref. This HIP states the two facts that define it: the tenancy key is the USER, not the org, and the write is a shallow merge so no surface can delete another surface's keys.

Motivation

Each app keeping its own copy of a person's settings in its own localStorage makes the same person look like two different users depending on which tab they are in (apps/pref/prefs.go:7-11). One store behind the user menu ends that. It is deliberately NOT settings: settings is per-org, per-product configuration with KMS custody for secret fields; collapsing the two would put one user's theme under an org key and make an org admin the owner of everyone's UI (apps/pref/prefs.go:31-35).

Specification

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

The store

One system-namespace SQLite file, prefs.db, opened through sqlpool.Open — born encrypted under the cek-derived key, single-connection (apps/pref/store.go:47-58). One row per subject, holding an opaque JSON document the server bounds but never interprets. It MUST hold no secret: a preference that needs custody does not belong in this table.

Addresses

Two operations at one path:

  • GET /v1/pref — a typed op. A caller who has never saved anything gets an empty document at 200, never a 404, so the user menu always renders.
  • PATCH /v1/pref — an untyped handler declared with prose (apps/pref/prefs.go:115-128), because three of its wire facts are unreachable from a typed op (apps/pref/prefs.go:148-165): a 16 KiB request-byte cap answering 413, an empty or literal-null body answering 400 where zip's decode would make both a successful no-op, and an OPEN key space whose only carrier — map[string]any — publishes no request body at all.

PATCH, not PUT: a surface saves the keys it owns without having to send back keys it does not know about. The merge is shallow, a named key is replaced whole, a null value deletes its key, and the merge runs inside one store transaction so two tabs saving different keys both survive (apps/pref/store.go:93-101). A document over 16 KiB or 128 keys is refused.

Tenancy

The subject is the canonical <owner>/<name> identity built from values the identity boundary minted from a validated credential (HIP-0026), and it is the mandatory predicate on every store statement (apps/pref/prefs.go:202-215). The bare user name is not unique across orgs — hanzo/z and admin/z are two people — which is why the org qualifies the key. There is deliberately NO path to another user's preferences: not for an org admin, not for a platform SuperAdmin, because no operational task requires reading someone's theme. An unvalidated principal is 403.

Money, events, telemetry

Free, said in those words: plugin/pref/main.go declares cloud.Free and the app appears in no metered list. It publishes nothing to the bus, so a customer's webhooks receive nothing from it. It emits nothing beyond the request span every route gets.

Stage

ga: the personal half of the identity core — every signed-in surface reads it — and its manifest row (manifest/apps.go:362) declares no stage.

Upstream

Derives from none. The store is the platform's own encrypted SQLite through the one shared opener; nothing external is forked, embedded or mirrored.

Rationale

The alternative tenancy key is the org, which every sibling store uses, and it is wrong here in a way that matters: preferences are personal, and an org key makes the org's admin their reader. Keying on the qualified user and refusing every cross-read — rather than gating cross-reads behind admin-ness — removes the check that could be forgotten. The alternative to server-side merge is client-side read-modify-write, which is a lost update the moment two tabs save different keys.

Security Considerations

The wrong implementation is an identity fold. Key on the bare user name and two users named z in different orgs read and overwrite each other's documents — which is why the subject is <owner>/<name> and both halves are length-bounded before use, so an oversized forged header cannot become a giant primary key (apps/pref/prefs.go:199-214). The document bound is the other exposure: an unbounded personal, unaudited row becomes free general-purpose storage, so the 16 KiB / 128-key caps are enforced on the raw bytes before any parse.

References

  • HIP-0026 — Identity and Access Management
  • HIP-0106 — The Hanzo Plugin Contract
  • HIP-0139 — Capability

Released under CC0 1.0 Universal Public Domain Dedication.