hanzo.yml stops publishing the o11y-site Sites project: o11y.hanzo.ai,
obs.hanzo.ai and dashboards.hanzo.ai redirect to platform.hanzo.ai/o11y.
Sign-out goes to IAM's end-session endpoint instead of a forward-auth's
/__guard/logout, and the "local" authz comment states the boundary as it is:
only cloud reaches a standalone runtime, with headers it minted from a
validated IAM bearer.
Co-authored-by: Hanzo Dev <[email protected]>
The {{$variable}} rewrite turned the Prometheus spelling into {{index .Labels "labels.service"}}, which no series carries, so every rule written that way rendered the label empty (" has failed its health probe"). Both $-forms now strip the labels. prefix, and {{ $x }} with spaces reads its name from the match rather than from trimming the braces.
Co-authored-by: Hanzo Dev <[email protected]>
The event plane folded every occurrence into event.fact, and the Sentry
module still read and wrote event.error. Its reads now select the error
signal of event.fact, mapping issue, env, origin and the severity band.
Errors enter through /v1/event only. The Sentry envelope and store
routes, DSN keys and their rotation leave the module; the consumer of
event.error files each fact through Ingest(org, product), which keeps
the issue lifecycle and returns the issues a batch created.
The test's organizations table now answers another org for the first row,
so it fails on any code that addresses alerts to the first org rather than
the rule's own.
Co-authored-by: Hanzo Dev <[email protected]>
SendAlerts addressed every alert to `SELECT id FROM organizations LIMIT 1`,
which is whichever org sorts first, not the rule's. That org's
alertmanager has none of the rule's channels or route policies, so the
alert sat on its default receiver and reached nobody, and that org's
members could see it. Alerts now carry the rule's own org id.
Co-authored-by: Hanzo Dev <[email protected]>
GetRecentLogs selected timestamp, resources_string and resource, which
event.log does not have, so every call answered 500 "Unknown expression
identifier timestamp". It now selects time, service and the k8s labels
from the attributes map, and binds org, the window and the limit as
arguments. The route answers 403 when the request names no tenant.
Co-authored-by: Hanzo Dev <[email protected]>
The v5 logs and traces statement builders read event.log and event.span,
where every row carries its org. Each read now binds `org = ?` to the
tenant: the validated X-Org-Id for a request (set by the Hanzo IAM
identity), or the rule's org name for a scheduled rule that reads logs or
traces. A read with no tenant is refused as forbidden instead of reading
every org. This covers list, time series, scalar and trace requests and
both span reads of the trace operator.
Co-authored-by: Hanzo Dev <[email protected]>
GET /v1/o11y/dashboards answers the V2 list, {dashboards, total, tags}, a page
at a time, and api/v1/dashboards/getAll still handed its callers data as if it
were a bare array. Home sorts it on first render, so a signed-in person landed
on "Something went wrong" (TypeError: s.data.sort is not a function) as soon as
the gui config stopped failing ahead of it. The export panel, the assistant's
dashboard picker and the legacy list read the same value.
getAll now reads every page of the V2 list, newest first, and maps each item to
the Dashboard those screens render: title from the display name, falling back to
the name, tags as the V2 list page shows them, and no body, which the screens
that need one fetch by id.
Co-authored-by: Hanzo Dev <[email protected]>
event.fact holds a sixth signal, economic: one payment between two orgs,
written into each party's partition by the cloud event app. It landed with
no TTL clause, so every payment was kept forever and the table's own rule —
every signal it can hold has a retention — no longer held.
0004 restates 0002's four clauses unchanged and adds economic at ten years
from ingested_at: seven years from an extended return's filing (26 CFR
1.6001-1(e); IRS "How long should I keep records"), plus the 21.5 months a
return can trail the payment, with a year to spare. It writes no part on
disk, and a guard refuses the run if the clause did not apply.
Verified on ClickHouse 26.8: an economic part eleven years old is dropped,
one nine years old is kept, act keeps its two years.
Co-authored-by: Hanzo Dev <[email protected]>
The gate calls hanzoai/ci [email protected] with linux-amd64 as the only
architecture that gates, and linux-arm64 as an advisory leg that runs and
reports without holding anything. The o11y pod is scheduled on amd64, and the
one shared arm64 node kept the release queued for hours behind other repos.
hanzo.yml states version 1.5.89. The two images' series had drifted apart
(o11y at 1.5.88, o11y-site at 1.5.82) and far ahead of the git tags (v1.5.75), so
a derived number differed per image and a git tag would have overwritten an
August image of the same name. imgver publishes the declared version once and
the next patch thereafter, for both images alike.
Co-authored-by: Hanzo Dev <[email protected]>
o11y.hanzo.ai serves its console from s3://hanzo-sites/hanzo/o11y-site (universe
static-sites.yaml, o11y-site-static), and only /v1/o11y reaches the pod. No lane
wrote that prefix: the o11y-site image builds the same export and nothing routes
to it. `site:` in hanzo.yml hands the export to hanzoai/ci's site lane, which
publishes a release tag and main through /v1/projects/o11y-site.
Co-authored-by: Hanzo Dev <[email protected]>
The console renders @hanzo/ui's Button, Badge and Toaster, which are @hanzo/gui
components, and gui reads its config from module state createGui fills. Nothing
here created one, so the first of them to render threw Err0 and the console
fell to "Something went wrong". providers/Gui mounts GuiProvider with the
fleet's config, @hanzo/ui/gui-config (the table console and hanzo.ai mount),
themed by this app's own dark/light switch.
The lockfile is regenerated so it installs at all. The catalog commit moved
every version into pnpm-workspace.yaml at the newest release (react-router 5 to
7, antd 5 to 6, redux 4 to 5, i18next 21 to 26, fifty-three majors) without
regenerating the lockfile, so `pnpm install --frozen-lockfile` has refused
since, and neither Dockerfile.site nor a site build could run. The code is
written against the versions the lockfile holds, so the catalog now states
those, and the design system stays at the catalog's line: @hanzo/ui 8.27.0,
@hanzo/gui 8.3.1. One copy of each installs, with every @hanzogui package at
8.3.1, where the old lockfile split them across 8.0.0 and 8.0.1.
chokidar 4.0.3, which the old lockfile already installed under sass, is excluded
from the trust policy by exact version: it is the last 4.x and went out without
the provenance 4.0.1 carried.
Co-authored-by: Hanzo Dev <[email protected]>
Every release since v1.5.72 died in this gate on three tests pinned to answers
the toolchain no longer gives, so no image or site was published for any of them:
- binding: from Go 1.27 encoding/json names a field from the value whose
UnmarshalJSON failed, so a nested custom type reports 'a', not 's.a'.
- publish: zip 1.37 answers a miss or a refused method with an RFC 9457 problem
document, and /mcp with plain application/json, protocol 2026-07-28 and the
jsonrpc member first.
- wiring: the console catch-all is registered through app.Raw(zip.MethodAll,
"/*", …) now, so the order check finds it by its "/*" path, not by the verb.
Co-authored-by: Hanzo Dev <[email protected]>
The guard once asserted a name where it now asserts the IAM subject, so a row
seated then is keyed by uuid5 of the name. The subject misses that row, the
create collides on the email, and ensureUser answered the collision with
success, leaving GET /users/me and every row keyed to the person on an id no
session asserts.
On that collision ensureUser now calls user.Setter.RekeyUser: in one
transaction it finds the org's row by email and moves it, its user_role rows and
every other row that references users.id onto the subject. SQLite checks the
foreign keys at COMMIT (defer_foreign_keys), so a reference left behind refuses
the whole move. The role grants follow the rows once they commit.
Co-authored-by: Hanzo Dev <[email protected]>
The derived-fields guide they pointed at lived on o11y.io, which
resolves to no server and is registered to another company. The
waterfall span's calculated-fields note and the ten CalculatedFields
descriptions now say what each field is and link nothing. That note
is what docs.hanzo.ai prints for the waterfall API's db_name, through
cloud's openapi.yaml.
Co-authored-by: Hanzo Dev <[email protected]>
An op learns its address from zip.OpOf and zip.AddressOf, so the claim table,
the five declaring verbs and the address bookkeeping are gone. A host that
declares one of these addresses itself now gets a Build conflict from zip.
Co-authored-by: Hanzo Dev <[email protected]>
A handler reads the operation it is serving and the address the call was made
to from the context zip states them in.
Co-authored-by: Hanzo Dev <[email protected]>
It installs no middleware anywhere, so registering routes is all it does and it
has no reason to hold the host. Taking the group says that, and it is what a
confined subsystem has to give.
Co-authored-by: Hanzo Dev <[email protected]>
The five claim verbs need the address an operation will answer at, which is a
root joined with a path. They used to ask the target for its prefix through a
struct written to answer with one — and that struct existed for a second reason
that has since gone: an operation with no declared id took a shape-derived one
qualified by the prefix of the occurrence it answered under, so a plain group
would have renamed every undeclared id. All 319 operations state their own id
now, so a declared id survives composition verbatim and the group is just a
group.
The roots are this package's own constants, so each group carries the one it was
made with and nothing can disagree about the address. The escape hatches name
their whole address and register through Raw, which says which method each one
answers.
The test that measured the gap reached zero and said so; it now holds the
property from the other side, that no operation goes undeclared.
Co-authored-by: Hanzo Dev <[email protected]>
The skip blamed emersion/go-smtp v0.24.0 for an error string rendered with
quotes. The quoting was Go's: 1.26 changed net/textproto.Error.Error to
"%03d %q". ef13a025e8 had already stopped the test reading rendered text
ten days before the skip was added, so there was nothing left to exclude.
A gate allowed to exclude from the suite is a gate that does not measure
the suite. Measured first: -count=10 -run '^TestEmailRejected$' passes
against the pinned v0.24.0, and the full suite passes with no skip.
One toolchain across the estate. The tree carried 1.26.0, .1, .3, .4 and .5
at once, so which compiler built a binary depended on which repo it came
from.
Co-authored-by: Hanzo Dev <[email protected]>
Idempotence is a property of a target, not of a statement. The four
re-derives collapse in event.fact because Replacing collapses them, and a
materialized view attached to that table never sees the collapse — it
fires on the inserted block, so every re-derived row reaches whatever
engine sits downstream. The three rollups this file owns count ids rather
than rows and are exempt by construction; a consumer it did not write has
semantics it cannot know, so the statement throws and names it.
The measured populations come out of the prose for the same reason step 5
refuses to write a row count into a comment: an earlier draft said 9,130
traces and 68 pairs against a live 168,151 and 300. The ONE row and the
TWO stay, because nothing writes them and they do not move.
A row count also says a table is empty, not that nothing reads it. Three
consumers still spell the columns step 4 drops and renames, and a query
log over 20.1 million queries cannot clear them: an unwalked path and an
absent path leave the same trace.
Cherry-picked from event/backfill-consumer-gate, minus its revert of the
0001 caveat and of endpoint back to door.
o11y carried a second identity system: email/password sign-in, Google,
OIDC and SAML callbacks, self-registration, invites, reset-password
tokens, roles-on-users, SSO auth domains, and a pair of tokenizers that
minted and stored its own sessions. Hanzo IAM answers all of it, once,
at the edge. Two credential stores is one too many, and the one nobody
signs into is the one that rots.
What is left is Identity: WHO a request is, resolved from what the edge
asserted or from a service-account key, never issued here. The routes are
not merely unrouted — the handlers, stores, tables and templates behind
them are gone, and anonymous_test.go names each dead address so that
re-exempting one fails even though no route can come back.
The census moves with it: 366 addresses to 329, 353 typed ops to 319, ten
escape hatches to seven. zipdoc_gen.go loses the 34 descriptions whose
addresses left, because zip keys its doc registry by method and path, so
a description outliving its declaration attaches this module's prose to
whatever a host serves at that address. zipdoc_test.go pins that.
Cherry-picked from delete/o11y-identity 89854a68ae.
HIP-0132 unified o11y_logs, o11y_traces and o11y_metrics into one `event`
database and dropped them. The commit that restored the reads repointed
three files and said so; it was thirty. The rest went on naming databases
that no longer exist, so trace funnels, the Kafka and queue panels, the
v3/v4 logs and traces builders and the PromQL adapter each answered
Code: 81 UNKNOWN_DATABASE, and the golden tests agreed with them because
they pinned the same dead SQL.
pkg/telemetryplane is the one address for the name, imports nothing, and
is reachable from every depth that needs it. Its test walks the tree and
fails the build on a literal that names a dropped database, tests
included — a golden encoding broken SQL is a second vote for the bug, not
coverage.
Two service views turned a failed read into an offer to instrument an
already-instrumented application. A failure now shows the failure, and
onboarding is offered on a successful empty read.
Cherry-picked from salvage/blue-wt-o11y-readers 83ecbec03c.
Completes the canonical verb set (help/build/test/lint/clean/dev). Alias only —
go-run-community stays the one recipe. Community is the variant build ships.
(cherry picked from commit 7dde97cd01)
Co-authored-by: Hanzo Dev <[email protected]>
The four names every Hanzo repo answers to, mapped onto what this one
already does — aliases, not second implementations:
build -> go-build-community-$(ARCH) test -> go-test
lint -> golangci-lint (.golangci.yml was committed; nothing ran it)
clean -> ./target + frontend/build, both gitignored. Never node_modules.
build is the SERVER binary for this host, because that is what the shipped
image runs: the root Dockerfile builds ./cmd/community and runs it headless.
The SPA is a separate image (Dockerfile.site) that builds frontend/ from
source in node:22, so js-build is not folded in — it is not part of this
artifact and it needs a newer node than the backend.
The first rule in the file was $(TARGET_DIR), a mkdir, so a bare `make`
built nothing and left an empty ./target. Nothing calls it that way — there
is no .github/workflows here and .hanzo/workflows runs go directly — so the
default is the help this file already had.
(cherry picked from commit 02a7431d4c)
hanzoai/ci keeps the reusable at .github/workflows/build.yml — the one path
both planes resolve, since github.com reads a reusable only from there and the
forge reads it too. The v1 tag moved with it, so a caller still naming
.hanzo/workflows/build.yml asks for a blob that is not in the tree:
InsertRun: read hanzoai/ci@v1:.hanzo/workflows/build.yml: object does not exist
The caller itself stays at .hanzo/workflows/cicd.yml on purpose: github.com has
no runner for these labels, so a caller placed there could never be scheduled.
Co-authored-by: Hanzo Dev <[email protected]>
Ranges drift once nothing moves them: a caret that has not been rewritten
since it was written pins whatever was current that day, and an exact pin
cannot even take a patch. Both are updated here.
One line per package, so a surface reads the same components, the same
tokens and the same face as every other one.
Ranges drift once nothing moves them: a caret that has not been rewritten
since it was written pins whatever was current that day, and an exact pin
cannot even take a patch. Both are updated here.
One line per package, so a surface reads the same components, the same
tokens and the same face as every other one.
One family. Font stacks name Zen and fall back to the native faces, tokens
name the role rather than a face, and the packages that used to supply a
face now point at @hanzo/font.
Self-hosted @font-face blocks are removed rather than renamed: rewriting the
family in one would leave a src pointing at a file this repo no longer ships.
@hanzo/font carries the binaries and declares the faces, so the local copies
go with the declaration that used them.
Comments naming a face we no longer use are deleted, not reworded.
The 2026-07-15 retag moved these published tags, so the proxy no longer serves
them, Go falls through to direct, and the bytes disagree with sum.golang.org.
Every build under the workspace stopped at 'SECURITY ERROR: checksum mismatch'
before reaching a compiler. Selecting versions the proxy still holds is the
repair; re-recording a hash or bypassing the notary is not.
A version written into a manifest is written into every manifest that wants it,
which is how eslint came to exist in a hundred and four forms across this estate
and how a package came to resolve twice at run time. The catalog moves the number
out of the manifest: a manifest names the package, the workspace names the
version.
Four are held below their latest because the newer release cannot be used, and
the reason is recorded rather than the version alone: @noble/hashes 2 renamed
every subpath the web3 stack imports, @splinetool/runtime 2 depends on a package
that was never published, @dynamic-labs 5.4.1 the same, and react-resizable-panels
3 points its types condition at the bundle.
Two defects, one of them silent.
tokens.js and tokens.d.ts still carried `Inter, sans-serif` in 200 places
while styles/tokens/style.css had already moved to Zen — the token module and
the stylesheet disagreed about what the product is set in. FONTFAMILY_INTER is
renamed to FONTFAMILY_ZEN rather than left as a name for a face it no longer
holds; nothing outside the token module referenced either name.
The MCPServerSettings sheets asked for var(--font-family-sf-mono), which is
defined nowhere in the tree. An undefined custom property is an invalid value,
so those rules had always fallen through to the bare `monospace` fallback and
rendered in whatever mono the OS supplies. They now read --font-family-mono,
which style.css defines as `Zen Mono, monospace`.
styles.scss sets Zen from @hanzo/font, and that package declares Zen, Zen Mono
and the Zen Pixel cuts — never Inter. Yet 79 declarations across 28 stylesheets,
the trace flamegraph's canvas label constant and the README banner all still
asked for Inter, and nothing here ships an @font-face for it. Those surfaces did
not fall back to Zen, they fell through to the browser default; the flamegraph
also measured its labels in a face it was not drawing.
The lockfile still pinned 1.8.0 while package.json asked for ^1.8.12, so the
CI=1 pnpm install in the Makefile had nothing consistent to resolve.
publish reached the declaration app through
zip.AdaptNetHTTP(adaptor.FiberApp(d.Fiber())) — out through net/http and back
into a router that is native at both ends. Per request that is: the live
fasthttp request rendered into an http.Request, then a second pooled
fasthttp.Request taking the method, URI, host and every header, the body copied
twice, r.RemoteAddr through net.ResolveTCPAddr, a second pooled
fasthttp.RequestCtx, and every response header copied back one at a time.
dispatch hands the request already in hand to that router, which is what a
listener does with one.
Composing instead is refused rather than chosen: the declaration names the same
367 addresses the service registers, and Build will not take a program where two
definitions claim one address. TestTheDeclarationCannotBeComposedIn measures the
refusal instead of asserting it in prose.
The probe group loses its per-request adapter. probe called zip.AdaptNetHTTP
INSIDE its own handler, so every liveness poll allocated one and re-wrote zip's
terminal marker into a process-wide map to answer a request whose shape has not
changed since boot. It and hatch now share the package's one bridge, and the
installed health handler is read once — where the answer is chosen — so a
SetHealth landing mid-request can no longer make the branch and the call
disagree. runtime.go's answers() had no callers and is gone with it.
Both wires were pinned before the change and pass after.
TestPublishServesTheDeclarationVerbatim asserts each control path's status,
content type, Allow, the absence of X-Content-Type-Options and the body —
literal where short, byte-equal to a declaration app's own answer where long —
and the miss a dot segment, a bare "." segment and a doubled slash each produce,
none of which this router cleans. TestProbesAnswerVerbatim asserts every probe's
whole answer in each of the four states a host can leave the module in, refusal
included: text/plain and nosniff, which is what says an answer came from the
net/http side. The five single-fact health tests it subsumes are gone.
styles.scss declared two @font-face rules pointing straight into
node_modules/geist/dist, so the observability console every operator lives in
was set in Vercel's face — 604 references to 'Geist Sans' and 'Geist Mono'
across 177 files, plus a `geist` npm dependency and a `--font-family-geist`
token that named it in the design-token layer too.
@hanzo/font's ./css export is plain @font-face rather than a next/font loader,
so a Vite app imports it directly and vite emits the woff2 locally — the same
one import the admin shells use. The families are Zen and Zen Mono, the tokens
are --font-family-sans and --font-family-mono, and the geist dependency is gone.
The nine remaining 'Inter' declarations went with it; nothing loaded that face
either, so those blocks were rendering in whatever the platform's sans is.
The sweep left these: comments calling an address that accepts requests a door,
and one saying a writer is refused "at the door" where it means the edge.
Test fixture values ("seam-door", "sub_door") and the English idiom "opens the
door for other business opportunities" stay — neither is the metaphor.
go build ./... exits 0 for both. o11y's `go vet` reports unreachable code in
generated parsers, which it reports identically on unmodified HEAD.