The cdn.gpteng.co tag is gone from index.html along with the CDN font import, so script-src 'self' is now satisfied outright rather than blocking something the page still carried.
7.1 KiB
hanzo.sh
The install surface: one page that is also one installer. curl hanzo.sh | bash
installs the Hanzo toolkit; opening hanzo.sh in a browser renders the landing
page. Both are the same file.
The polyglot
pnpm build is vite build && node scripts/build-polyglot.js. The second step
rewrites dist/index.html into a file that is simultaneously:
- a shell script — line 1 is
#!/bin/sh, line 2 opens a<<\EOFheredoc that swallows the whole HTML document, and the installer (public/install.sh, minus its own shebang) follows the closingEOF; - an HTML document — the heredoc content is the real page, and
</html>is rewritten to</html><!--so the shell half is an HTML comment.
The installer body uses bash arrays, so the contract is curl hanzo.sh | bash,
not | sh. The Dockerfile asserts all three properties (shebang, <!DOCTYPE html>, bash -n) and fails the build if any is lost — nothing downstream can
catch it, because everything downstream is bytes.
public/install.sh is also served at /install.sh for anyone who wants the
installer alone; public/{bot,cli,agents,dev,full,mcp,node,python,rust} are
per-bundle shims.
Serving chain
push to main
-> .github/workflows/sync.yml carries refs to git.hanzo.ai (nothing else)
-> .hanzo/workflows/deploy.yml runs natively on the forge
docker build . Dockerfile: pnpm build -> polyglot assert
-> FROM ghcr.io/hanzoai/static:v0.5.1
docker push ghcr.io/hanzoai/hanzo-sh:<short-sha>
-> hanzoai/universe infra/k8s/operator/crs/hanzo-sh.yaml
spec.image.tag: <short-sha> set by a human, never by the build
-> hanzoai/ingress hanzo.sh -> Service hanzo-sh:80 -> :3000
hanzoai/static is a Go binary on scratch. It defaults to -port 3000 -root /public and answers with http.ServeContent, so a file goes out byte-for-byte
with no rewrite, minify or injection — which is the only reason a polyglot can be
served from an ordinary static server at all. It runs WITHOUT -spa: this app has
exactly one route, so a mistyped path must 404 rather than return the installer.
HANZO_STATIC_CSP in the CR is a response header only; it cannot touch the body.
It sets script-src 'self', which the page now satisfies outright: the
third-party cdn.gpteng.co/gptengineer.js tag is gone from index.html (it was
a build-tool artifact the CSP was already blocking and nothing depended on), and
so is the CDN font @import.
Not live from here yet
hanzo.sh is served today by a Cloudflare assets Worker named hanzo-sh
(wrangler.toml, custom_domain = true), published by hand with
pnpm build && npx wrangler@3 deploy. wrangler.toml stays until the image is
promoted; deleting it first would strand the live host.
Two things must be reconciled BEFORE anyone pins a tag in the CR:
- The CSP can come down to same-origin.
mainused to@importGeist fromcdn.jsdelivr.net, which is why the CR's CSP allows that host for styles and fonts. It does not any more:@hanzo/designships both Geist faces aswoff2inside the package and the build emits them intodist/assets/, so the page makes no third-party request at all. Dropcdn.jsdelivr.netfromHANZO_STATIC_CSPwhen the image is promoted. - Promotion order. publish an image -> set
spec.image.tagincrs/hanzo-sh.yaml-> add- hanzo-sh.yamltocrs/kustomization.yaml-> confirm the pod is Running and thatcurl <pod>/ | bash -s -- --helpprints the installer usage -> only then repoint hanzo.sh DNS off the Worker -> then deletewrangler.toml.
The CR is committed INERT (empty tag, absent from kustomization.yaml).
Promoting an App with no image tag takes the host down instead of leaving it
alone.
Stack
Vite 5 + React 19 on @hanzo/ui over the @hanzo/gui backend. One route
(src/pages/Index.tsx), no router — hanzoai/static runs without -spa, so the
routing decision is already made and a client-side router would only pretend to
make it again. pnpm 9; pnpm-lock.yaml is what the build resolves
(--frozen-lockfile).
pnpm install
pnpm dev # vite, :8080
pnpm build # -> dist/, including the polyglot rewrite
pnpm typecheck
pnpm lint
scripts/polyglot.sh is an unwired duplicate of scripts/build-polyglot.js;
only the .js is in pnpm build.
Where the page comes from
There is no Tailwind, no shadcn and no Radix; the 48 generated components/ui/*
files and the 27 Radix packages behind them are gone.
- @hanzo/products — the header's launcher and the whole footer index. The
content of both is settled once for the estate; hanzo.sh only declares its own
four nav links and its one call-to-action (
src/site.ts), because@hanzo/[email protected]has nohanzo.shSiteIdyet. When it does,HEADERcollapses toHEADERS['hanzo.sh']. - @hanzo/ui/product —
SiteNav,SiteFooter,BrandMark,Panel,StatusTag,ToastProvider. The page used to draw two headers and two footers (aNavbarabove aHerothat carried its own header, and a hand-written footer ofhref="#"links below the real one); these are the components that end that. - @hanzo/gui — every primitive under it (
YStack/XStack/Text/Anchor/Separator), on the shared scale from@hanzo/ui/gui-config. - @hanzo/design — the token layer: monochrome, dark by default, Geist Sans
and Geist Mono self-hosted in the package.
src/index.cssimports it and does nothing else. - @hanzo/logo — the mark, through
BrandMark. Not a direct dependency:@hanzo/uialready depends on it, and declaring a package nothing here imports is how a dependency list starts lying.@hanzo/brandis deliberately absent too — it is the MULTI-brand registry, and hanzo.sh is one brand. Reaching for it here is how a Lux or Zoo mark ends up on a Hanzo surface.
src/site.ts is the page as data; src/components/ renders it. Responsiveness
is flexWrap + minW, not breakpoints: the same tree reflows from 390px to
1280px with one media prop in the whole app.
Two workarounds, both with a delete condition
@hanzo/[email protected] has two defects this app has to route around. Both are marked
in place; remove them when the package is fixed.
src/shims/hanzogui-next-theme.ts—@hanzo/ui/productstatically imports@hanzogui/next-theme(an optional peer whose entry importsnext/script), because tsup putThemeToggleandThemeToggleNextin one chunk. No Vite app can resolve it.vite.config.tsaliases the specifier to a stub; nothing here renders a theme toggle.src/gui.config.ts—SiteNavwrites$smmeaning "phone", but v5 definessmasminWidth: 640, so on the stock scale a phone gets the desktop link row and a desktop gets a hamburger. This app redefines that ONE key asmaxWidth: 639.98; nothing else it renders readssm.
Also unusable in 8.0.26: @hanzo/ui/core, /tokens, /gui, /shadcn,
/components, /models, /primitives — the exports map points them at src/,
which files: ["dist"] does not publish. src/mono.ts reaches the mono family
through @hanzo/design instead.