Files

6.6 KiB

LLM.md — hanzoai/works

hanzo.works is the business face of Hanzo: the products a company installs and uses. hanzo.ai sells the platform, hanzoskills.com is agent-facing markdown, this is what a business buys and opens.

Content model

Two data files are the only content source.

src/products.ts holds what the page shows. sections are the products a company installs or opens — each with the address that serves it. rows are the business apps, grouped by what they are for. A product goes in when it is running. Verify with a request before adding one: if the address does not answer, it does not go on the page.

src/integrations.ts holds the works-with bar. Every entry is a connector that ships in hanzoai/extension packages/, or a channel Hanzo Bot answers on. Marks come from simple-icons (CC0) and are inlined at build time, so the export carries the ~19 paths it draws and the library stays out of the bundle.

Some tools we connect have no freely licensed mark — Salesforce, Slack, Microsoft Teams, Outlook, Office, DocuSign, Canva, Workday. They are named in alsoConnects and not drawn. Do not source those marks elsewhere; the reason they are absent from simple-icons is the reason we do not draw them.

Adding a product means adding it to src/products.ts and public/llms.txt. The gate llms.txt names every product the page shows fails otherwise, which is the drift that matters: the page telling a person about something it never tells a model.

Design

Radii are the same tokens hanzo.ai renders: buttons are --radius-full, cards are --radius-2xl. A gate measures the computed border-radius rather than the class, because a square button is the defect and a class name cannot show it.

The palette, type ramp and hairlines are @hanzo/design tokens. Nothing here invents a value.

@hanzo/tokens is not a dependency. @hanzo/design publishes the token layer in styles.css. Loading both defines two sets of custom properties for one design system (--background against --hz-background) with nothing keeping them in agreement.

@hanzo/appearance does not do light/dark. Its scope is a person's type scale, density, face, measure and accent. Light/dark is the design system's own contract: dark is :root, light is .light on the root element. applyTheme() from @hanzo/iam/react is the one control; setTheme in components/Chrome.tsx mirrors its answer onto .light. One value, one key (hanzo_iam_theme), read by both the head script and the control.

@hanzo/iam and @hanzo/logo ship no "use client" directive. components/Chrome.tsx is the one boundary that imports them; everything else stays a Server Component and ships no JavaScript.

Auth

This page holds no identity. Sign in is a link to console.hanzo.ai, which authenticates against Hanzo IAM. There is no password field, no form, no session minted here and no token verified here, and a gate asserts all four.

It does not run its own OAuth client, and should not grow one. A marketing page has no signed-in state worth showing, so a client of its own buys a name in the corner and costs an IAM application registration — which is a provisioning dependency that breaks the button when it is missing. Every product this page links to signs its own visitors in.

@hanzo/iam stays a dependency for applyTheme/readThemeMode only.

Config

headers() is not in next.config.mjs. It is inert under output: export, so a block there would read as a security control and enforce nothing. Response headers belong in a headers middleware in front of the static one on the serving side — public/_headers is not read by hanzoai/ingress.

trailingSlash: true is load-bearing. It makes the export a tree of directory indexes, which is what a plain file server resolves. The flat form emits siblings like foo.html, which 404 when asked for as /foo/.

.hanzo/workflows/, never .github/workflows/. The forge reads the first workflow directory that exists. hanzo-build-linux-amd64 is advertised only by runners registered to git.hanzo.ai, so the same job on github.com is never assigned and waits out a 24h timeout instead of failing.

Gates

Playwright against out/ — the bytes that ship, never a dev server. They assert paint and measurement, not markup: the body's computed background is the token's value, buttons measure as pills, the marquee is wider than its window and animating, the page does not scroll sideways at 390.

every host this page sends a reader to answers walks the real network. It is meant to fail when a product goes down.

Theme tests STATE the colour scheme. Playwright's Desktop Chrome reports prefers-color-scheme: light, so a test that just loads the page measures the runner's preference and calls it the site's.

Serving

The site is bytes in S3 read by hanzoai/ingress, not an App. No image, no CR, no replicas.

Piece Where
Publish .hanzo/workflows/deploy.yml → hanzoai/ci site@v1 → s3://hanzo-sites/hanzo/hanzo-works
Route universe charts/app/values/hanzo/static-sites.yaml — middleware + route, auto-syncs
Cert universe infra/k8s/ingress/wildcard-certs.yaml — wildcard-hanzo-works, auto-syncs
DNS reconciled from the route — see below

spaMode: false. The export is trailingSlash: true directory indexes with a real file per route; an SPA fallback would answer 200 with the homepage for every missing chunk.

Do not write a infra/cf-zones/hanzo-works.yaml. The cloud binary reads CLOUDFLARE_API_KEY/CLOUDFLARE_EMAIL from KMS on a loop and reconciles Cloudflare records from the IngressRoute — apex and www A records for this host appeared on their own once the route synced, proxied, pointed at hanzo/ingress-lb. A zone file would be a second owner of records something else already writes. The four zone files that do exist predate this.

Response headers are the hanzo-works-headers middleware on that route, and they have to be: staticFiles reads objects out of S3 and never a _headers file, so a header the site wants is a middleware or it does not exist. The CSP is enforcing and names hanzo.id and api.hanzo.ai because those are the only origins the page reaches.

Read the ingress LB address live rather than from a file — DigitalOcean recycles released IPs:

kubectl -n hanzo get svc ingress-lb -o jsonpath='{.status.loadBalancer.ingress[0].ip}'

The repo is on github.com only. git.hanzo.ai/hanzoai/works does not exist, and the forge is where CI runs, so .hanzo/workflows/ is dark until the forge repo is created and set as the push target. Until then a deploy is manual: build, then sync out/ to the S3 prefix.