"Forge" named nothing in this estate. The product is Hanzo Git, the address is
git.hanzo.ai, and the in-cluster service is hanzo-git.hanzo.svc. A generic word
invented for it sends a reader looking for a system that does not exist.
Comments only. ("unforgeable" stays — different word.)
Every SDK locked its spec to hanzoai/cloud. That repo is a 12-commit stub with
no openapi.yaml in it at all, so the raw fetch answered 404 and the client lane
has failed on every push since. The document is in hanzo-inc/cloud — 10,618
commits, where the locked ref resolves and openapi.yaml at it hashes to exactly
the sha256 this lock already names.
Naming the real location does not by itself restore the lane: that repo is
private, and the hanzoai token reads it as not-found. But a lock pointing at a
repo with no document can never work, and this one works the moment the token
can see it.
Co-authored-by: Hanzo Dev <[email protected]>
hanzo_mcp.zap_server imported hanzo_tools.browser.zap_server, which does not
exist — the module could not be imported at all. Its caller swallowed that in
`except ImportError: return`, so _start_zap_server() ran on every `run()` and
started nothing, and the HANZO_NO_ZAP switch guarded a path that never had an
effect either way.
The browser tool reaches Chrome through the native host -> zapd -> the socket
at ~/.zap/run/zapd.sock, where hanzo-mcp is a consumer (hanzo_tools.browser
.zapd_consumer). That is the transport, so the WebSocket server and its mDNS
discovery go, along with _normalize_mcp_result, which existed to shape results
for that wire and had no other caller.
hanzoai/runtime is archived. The subcommand set up a checkout at
~/work/hanzo/runtime and installed the hanzo-runtime / @hanzo/runtime
SDKs — a source tree that no longer exists and packages that were never
really released (npm only ever had 0.0.0-dev; PyPI's last release was
2025-07-15).
Verified: hanzo.commands.install imports clean and install_group now
exposes 13 subcommands with 'runtime' absent.
(cherry picked from commit 16d31994dd)
Co-authored-by: Hanzo Dev <[email protected]>
.generated names every path generate.py last wrote here. CI clones fresh, so a
manifest that exists only on the machine that ran the generator leaves the
driver unable to tell what it owns — and a file that stops being generated
stops being removed.
The four other clients already track it; this one did not.
RELEASE.md described GitHub Actions, GitHub secrets and three bin/ scripts
that each took a PyPI token off the environment. None of that publishes this
repo — .hanzo/workflows/publish-pypi.yml does, reading the token from KMS —
so the scripts are gone and the page describes the lane, its tag mapping, and
what to check before a tag makes a version permanent.
LLM.md carries the published state: hanzoai 3.2.13 with get_keys/get_tools,
and the KMS route that a 404 will otherwise misreport as a missing secret.
The job asked for /v1/kms/orgs/hanzo/secrets/<path>/<key> and parsed
.secret.value. KMS serves GET /v1/kms/secrets/<path>/<key>?env=<env> and
answers {"env","name","value"}, taking the org from the token's own claim,
so every read 404'd and every release reported the credential unseeded —
including hanzo/prod/python-sdk-publish/PYPI_TOKEN, which is there.
Root to 3.2.13, README install line names it. hanzo-research declares its
readme, license and authors, so its page carries them.
hanzoai/cloud@113cf7d6 declares components.securitySchemes.bearer and applies it
document-wide, so regenerating writes a populated Configuration.auth_settings()
and auth_settings=['bearer'] into 2498 call sites; ApiClient._apply_auth_params
sets Authorization: Bearer <token> from access_token, which was read by nothing
before. Four call sites carry an empty list — exactly the operations the document
marks security: [] (get_models, get_models_providers, get_commands,
get_openapi_json) — so the public routes still go out with no credential.
examples/client.py drops the header_name/header_value compensation and passes
access_token. examples/models is a new flow on the public catalog: it runs before
a reader has a key, which makes the install checkable in one command.
Proven on api.hanzo.ai, not by reading: examples.models prints 112 models with no
credential; examples.hello returns the caller's keys with a real sk- and 403
"sign in to manage API keys" with a bogus one.
The distributed inference, topology and device-capability code derives
from exo (github.com/exo-explore/exo), which was GPL-3.0 when this fork
was taken. The package declared BSD-3-Clause, which grants terms the
upstream licence does not.
Ship exo's GPL-3.0 text, add a NOTICE naming upstream and recording the
modifications, and declare GPL-3.0-only. hanzo-network stays an optional
extra with guarded imports, so the base CLI is unaffected.
openai_api_check.py was not a test — pytest never collected it, nothing imported
it, and its whole body was a hand-run connectivity probe against a vendor with
the credential written in as a literal. There is nothing to keep.
The value it carried must still be rotated: it is in this repo's history and the
history is public.
This script carried its key as a literal. A key in a source file is a key in
every clone of that file, and this repo mirrors publicly, so the value must be
rotated independently of this change — removing it from the tip does not remove
it from history.
Reads OPENROUTER_API_KEY and exits with a clear message when it is unset.
"Nothing in the client reads the environment" is true of the generated tree —
zero os.environ under hanzoai.cloud — and false of the package a reader
installs: hanzoai.zap reads HANZO_ZAP_ENDPOINT and hanzoai.config reads
HANZO_CONFIG_HOME. Neither carries a credential, which is the property the
sentence was reaching for, so it now says that instead of overreaching.
The README offered the two key shapes as a permission split — pk- to read,
sk- to write. cloud's own identity boundary says otherwise: middleware_identity
returns nil for any pk- before it consults the key store, because a key meant
for a browser bundle must not read. A pk- resolves to an org so a beacon can be
attributed to a tenant, and that is all it does.
Following the old sentence, a reader exports a pk- and every one of the five
flows answers 403 — /v1/tools spells out why, "a validated principal is
required" — and that refusal reads like a revoked key rather than the wrong
shape of key. examples/client.py now refuses a pk- before the request goes out,
the same argument that already rejects an unsendable access_token.
Also: three status codes in example docstrings had drifted from what the API
answers now (/v1/kv/keys/* 404s on every verb, /v1/automations/mcp 404s), and
two dead files went with them — examples/.keep, whose directory has held real
flows for a while, and scripts/mock, which reads a .stats.yml this repo does
not have and which scripts/test already says is unnecessary.
README: PyPI serves 3.2.1 and this tree is 3.2.11, so the install line is the
git one. Between the two the default version left the operation ids — 3.2.1
carries get_v1_keys/get_v1_tools (182 api modules, 2172 models) and this carries
get_keys/get_tools — so `pip install hanzoai` above a `get_keys()` quickstart
hands the reader a client that cannot run it. Counts are measured off the pinned
document: 1814 paths, 2479 operations, 191 tags + default_api = 192 API classes
over 2460 models, 2659 importable modules.
Auth was wrong everywhere and it was the same root cause: the document declares
no securityScheme, so every operation got an empty auth_settings and
Configuration(access_token=…) is read by nothing. Serializing get_keys from such
a Configuration produces a request with no Authorization header at all — the
five flows have been calling anonymously and reading the refusal as a bad key.
examples/client.py now passes the bearer through ApiClient's header_name/value
pair, verified against a local server that echoes what it received, and a smoke
test pins both halves so the day cloud emits the scheme, the test fails and the
header moves back into Configuration.
The examples gate resolved class names and stopped there, which is how `money`
and `tools` once passed it while every call in them named a method that no
longer existed. It now walks the syntax tree for attribute accesses on anything
bound to a *Api and asks the class: ten names resolve, and a stale get_v1_tools
fails in the same second the import passes.
Deleted five example scripts that imported hanzoai.Hanzo, hanzoai.cluster,
hanzoai.agents and hanzoai.completion — none of which exist — or were dict
literals captioned "in practice this would be called through the MCP interface";
compileall passed all five, because syntax is not a claim. Deleted three docs
that were fabrication rather than drift and that nothing rendered: FEATURES.md,
QUICKSTART.md, GPT5_ORCHESTRATION.md.
pkg/hanzo-agent said `pip install hanzoai` and `from hanzoai import Agent,
Runner`; the client has no Agent. Its own `full` extra resolved to
hanzoai[web3,tee,marketplace,cli], extras the client does not declare, so it
installed the client and none of the extensions. pyproject said BSD-3-Clause
while LICENSE is the Apache 2.0 text. test_smoke imported tracker_api and no
/v1/tracker path is emitted any more, so the suite carried a failing test naming
a module the client does not have; it reads analytics_api now, off the client.
uv.lock still carried 3.2.10 after pyproject moved to 3.2.11. A lock that
names a different version than the project it locks is a second answer to a
question with one answer.
A regeneration on 2026-08-09 claimed v3.2.10 and tagged it, but that commit
never reached main, so main went on declaring 3.2.10 too and I bumped onto
the same number. Two trees cannot share a version: the tag names the other
one.
The next free patch is what the version should have been. Read the TAGS, not
just pyproject.toml — the tag is where a version is claimed.
Twelve ids came from the document's own old->new map; the rest are generated
method spellings the map does not carry, read off the regenerated client.
Neither set follows a search-and-replace: the document renamed 2014 operations
and 21 of those do not reduce to dropping `_v1_` — /tasks and /meet became
`*_app` and `*_app_assets` when the SPA and the API stopped sharing a name,
and `get_v1_team_transactor_api_v1_statistics` drops only the first, the
second being part of the path.
Checked, not assumed: every `.get_*`/`.post_*` an example calls resolves
against the 7519 methods the client exposes.
Regenerated from hanzoai/cloud@6a719cdc (.spec-lock carries the ref and the
digest). 3.2.9 was a projection of a document 343 commits old, and in between
the operationId stopped carrying the default version: 5196 method names the
client published — `get_v1_tools`, `post_v1_agent`, every one of them — name
operations the document no longer declares. None remain.
The id is one identity, not four. An operationId is the SDK method, the MCP
tool name, the CLI spelling and the by-name key, so a client that keeps the
old spelling does not merely look stale — it teaches a name nothing answers to.
Shape: 1813 paths / 2479 operations -> 192 api modules + 2460 model modules,
all 2659 importable.
This repo is a fork, and its SECURITY.md still carried the upstream's contact —
so a researcher who found a hole in what WE ship was told to mail a vendor who
cannot fix it, and who would then know about a live vulnerability in our
production before we did.
Fourteen repos were in that state: datastore -> [email protected], git and
its two worktrees -> [email protected], auto -> [email protected],
calendar -> [email protected], dns -> [email protected], python-sdk ->
[email protected], plus bot, gui-gates, gui-geist, playground and
playground-oauthcb pointing at brand domains with no mailbox behind them.
All of them now carry one policy and one address, [email protected], matching
what cloud and gateway already used. The policy says out loud to report here
rather than upstream, because the next fork will inherit the same file and the
same instinct.
v1.0.49's client lane decided check-vs-move from `github.event_name`, which
git.hanzo.ai renders as `workflow_call` inside every reusable, so this repo's
pushes ran as MOVEs and went looking for hanzoai/cloud's newest tag. That tag
is v1.801.507, which is not on cloud's main and carries no openapi.yaml, so the
fetch 404'd and took the job with it.
v1.0.54 asks the payload instead: with none, the ref is the one .spec-lock
already names, which is what a check is and what a push wants.
The package is named for the cloud app it speaks to — cloud apps/sandbox is
hanzo-sandbox here — so there is one name per concept across the two languages.
Built against the routes rather than guessed:
GET|POST /v1/sandboxes list, create
GET|DELETE /v1/sandboxes/:id get, delete
POST /v1/sandboxes/:id/exec argv | command, stdin, dir, timeoutSec
GET|POST /v1/sandboxes/:id/fs read, write
The API key carries the org, and the client sends nothing else. cloud resolves
it through principal.OrgOf(user, org), which returns nothing without a validated
principal — "no validated principal, so the org claim is untrusted" — and the
gateway strips a client-supplied X-Org-Id before a handler sees it. A first cut
took `org` as a required argument and sent the header on every call; that would
have been ignored at best.
Two things the tests found, both real:
* with no key configured the client still sent `Authorization: Bearer `, an
empty credential that reads as a present one and hides "no key" behind
whatever the server says about a bad one. It sends no header now.
* cloud spells the size field `class`, which python cannot, so Sandbox maps it
to `cls` at the boundary instead of leaving callers to index a dict.
This is the REMOTE half. hanzo_sandbox.sandbox stays the local one — unshare,
namespaces, a boundary on this machine. Same word, unrelated concerns, separate
modules.
Served by a real HTTP server in the tests, since what breaks in a client of this
shape is the request: path, method, credential.
Co-authored-by: Hanzo Dev <[email protected]>
The regeneration onto hanzoai/[email protected] landed without moving this, because
the bump it carried named a version two releases behind: another line had already
cut 3.2.7 and 3.2.8 while it was in flight. The tree is right and the number was
not.
The ai door composes, so hanzoai/ai's own contracts reach this client for the
first time: 101 new model modules, 93 write operations that took an untyped bag
now take a declared body, and the fleet document types 1428 of its 2463
operations. 2624 generated modules import clean.
Diverged 2026-08-07 and each side did the SAME work. Both applied a commit
called 'one generated client in the wheel' — 2,213 files, 800,555 deletions,
and the forge retains nothing GitHub dropped. `git cherry` says the other
GitHub commit is already here under a different sha. So there is no
disagreement about the code, only two write sides doing it twice.
The forge then kept going: five client regenerations against
hanzoai/[email protected], 3.2.2 -> 3.2.7. GitHub stopped at 3.2.2.
Merged with -s ours: the histories join so GitHub can fast-forward onto this
and stop being a second head, and the tree stays the newer regeneration. Taking
the trees instead would resurrect ~140 generated modules that both sides had
already agreed to delete — the merge reported them as modify/delete conflicts
purely because the two deletions have different shas.
Co-authored-by: Hanzo Dev <[email protected]>
Cloud's APIKeyPrefixes admits pk- and sk- only (auth_identity.go, 4b030b77).
The lsp tool still told the user to set an hk- key, which cloud resolves to no
principal — following the message produced a key that could never authenticate.
The message is now a single NO_KEY beside the resolver that mints it, exported
from hanzo_tools.core, so a second tool reporting it cannot spell it differently.
Resolution order is untouched: HANZO_API_KEY, then ~/.hanzo/config.json .apiKey,
then the CLI's IAM session.
vector_index names the split cloud actually draws: pk- publishable for read,
sk- secret for write.
Co-authored-by: Hanzo Dev <[email protected]>
Cloud's APIKeyPrefixes admits pk- and sk- only (auth_identity.go, 4b030b77).
The lsp tool still told the user to set an hk- key, which cloud resolves to no
principal — following the message produced a key that could never authenticate.
The message is now a single NO_KEY beside the resolver that mints it, exported
from hanzo_tools.core, so a second tool reporting it cannot spell it differently.
Resolution order is untouched: HANZO_API_KEY, then ~/.hanzo/config.json .apiKey,
then the CLI's IAM session.
vector_index names the split cloud actually draws: pk- publishable for read,
sk- secret for write.
Co-authored-by: Hanzo Dev <[email protected]>
The document types 1495 of its 2459 operations, up from 1242. 87 new model
modules carry the response shapes ai, eval, functions, domain, experiments,
security and benchmark now declare, so those calls return a typed object
instead of a dict. 2524 generated modules import clean.
.spec-lock moves from v1.801.497 to @ffbc86064, which is v1.801.498.
`@v1` is a rolling alias and the mirror moves it back. Measured within four
minutes today: v1 was pushed to the commit carrying the fix, a sync put it back
on the commit from two releases ago, and the runs in between generated against a
pipeline that no longer matched the one just released — the client lane read an
OPENAPI its own reusable had never exported and died telling the caller to set a
variable the caller does not own. An immutable patch tag cannot be moved by
anyone, which is the only property this pin needs.
`python3 generate.py` finds no `yaml` module on a fleet runner, so three of the
four call sites could not run the driver they had just been handed. js-sdk
already ran it under uv and worked; this is that same line, in the other three.
`${OPENAPI:?…}` only prints its word when the variable is unset, but bash
PARSES that word either way, and inside it a lone `'` opens a quote that never
closes: `unexpected EOF while looking for matching '`, at line 23, with OPENAPI
set and the branch not taken. The regeneration died on a message about how to
supply a value it already had.
Two separate things kept this projection from ever moving.
The generator lives in hanzoai/openapi and this repo went and got it for itself,
with no credential, from a private repo — so the first dispatch that ever
reached the client lane died at `could not read Username`, exit 128. hanzoai/ci
holds the token that reads this forge, so it now fetches the driver once and
hands it over; a call site that does not clone cannot be wrong about where to.
And the trigger: `repository_dispatch` is a verb this forge does not have, and a
nightly `schedule:` decides when by a clock rather than by a release. The forge
answers a workflow_dispatch POST with 204 and a runner takes it in seconds, so
that is the one trigger left.
hanzoai/ci's client lane, given no payload, now reads hanzoai/cloud's newest tag
off git.hanzo.ai and regenerates this tree onto it. This is the trigger that
makes it happen without anybody pressing anything.
It replaces a fan-out that could not work in either direction: cloud pushed
`repository_dispatch: spec-update` at nine projection repos, but github.com has
zero self-hosted runners and these callers ask for the forge's pool, so an
accepted event queued until GitHub cancelled it a day later — and the forge,
which has the runners, has no repository_dispatch endpoint at all. Twelve such
dispatches exist across the fleet and not one reached a first step.
Asking is the better verb even where telling would have worked. It needs no
credential spanning six owners, it runs on the runners that exist, and a release
stays green whether or not this repo is well — a projection that is down is this
repo's problem, not a reason to redden a cloud release that is already live.
They shared a name and the lane died on it. `SPEC_REF` is the ref of the
DOCUMENT — hanzoai/ci's client lane exports a hanzoai/cloud sha or v-tag — and
this script also handed it to `git clone --branch` against hanzoai/openapi, which
is a different repository and has never had a ref by that name.
`git ls-remote hanzoai/openapi 5126939` returns nothing, and neither does
v1.801.495, so every CI regeneration died at the clone before the generator ran.
By hand it appeared to work, because with SPEC_REF unset the default was `main`
and both repositories happen to have a branch called that — which is the worst
kind of coincidence, one that makes a broken path look tested.
The generator is cloned at its own default branch now, from git.hanzo.ai, and the
document reaches the driver as a value: hanzoai/ci passes it digest-checked, and
without that the driver reads this tree's .spec-lock and fetches the same bytes
from the same forge. A hand run and a CI run project the same document instead of
whatever main happens to be.
This distribution shipped two complete generated clients under one import root.
`hanzoai.cloud` is the current one, generated from hanzoai/cloud's openapi.yaml at
the ref .spec-lock names. `hanzoai.api` / `hanzoai.models` was another, and the
package root re-exported its 2,202 names flat — so the obvious spelling,
`from hanzoai import ApiClient`, reached the second client, while
`hanzoai.cloud.ApiClient` reached the first. Nine core names existed twice as
distinct classes bound to distinct documents, and which one a caller got depended
on which import line they happened to write.
The second lineage was not an older copy of the first. It was a projection of a
document that no longer exists: of the 1,695 routes it binds, 243 are still in the
document at v1.801.495 and 1,452 are not. Eighty-six percent of it builds a
request the server answers with a 404. It had been broken for as long as it had
been shipping, and nothing said so, because a client only fails at the call.
So it goes, along with its runtime — api_client, api_response, configuration,
exceptions, rest — which `hanzoai.cloud` carries its own copies of. The package
root now exports `cloud` and nothing else. Nothing is re-exported flat on purpose:
a name that appears in two places is a name that can disagree with itself, and the
namespace already says which document a symbol came from.
The smoke tests pinned the deleted surface — they asserted more than 300 api
modules and imported `hanzoai.api.tracker_projects_api` by name, which is how a
suite can stay green over a client that 404s. They now assert the surviving one,
plus the invariant that made this possible in the first place: exactly one
generated package under `hanzoai`, and none of the nine core names re-exported
flat beside it.
This distribution shipped two complete generated clients under one import root.
`hanzoai.cloud` is the current one, generated from hanzoai/cloud's openapi.yaml at
the ref .spec-lock names. `hanzoai.api` / `hanzoai.models` was another, and the
package root re-exported its 2,202 names flat — so the obvious spelling,
`from hanzoai import ApiClient`, reached the second client, while
`hanzoai.cloud.ApiClient` reached the first. Nine core names existed twice as
distinct classes bound to distinct documents, and which one a caller got depended
on which import line they happened to write.
The second lineage was not an older copy of the first. It was a projection of a
document that no longer exists: of the 1,695 routes it binds, 243 are still in the
document at v1.801.495 and 1,452 are not. Eighty-six percent of it builds a
request the server answers with a 404. It had been broken for as long as it had
been shipping, and nothing said so, because a client only fails at the call.
So it goes, along with its runtime — api_client, api_response, configuration,
exceptions, rest — which `hanzoai.cloud` carries its own copies of. The package
root now exports `cloud` and nothing else. Nothing is re-exported flat on purpose:
a name that appears in two places is a name that can disagree with itself, and the
namespace already says which document a symbol came from.
The smoke tests pinned the deleted surface — they asserted more than 300 api
modules and imported `hanzoai.api.tracker_projects_api` by name, which is how a
suite can stay green over a client that 404s. They now assert the surviving one,
plus the invariant that made this possible in the first place: exactly one
generated package under `hanzoai`, and none of the nine core names re-exported
flat beside it.
Actions are disabled on this repository, so these files have not run in a long
time and cannot run: they are build definitions no forge executes. Builds go
through the native path -- POST /v1/runner, which schedules a BuildKit job --
and CI lives under .hanzo/workflows. A dead workflow that still looks like a
pipeline is worse than none, because it reads as coverage nobody has.
ff-main asked only "is LOCAL an ancestor of REMOTE" and called every other
answer DIVERGED -- so the forge simply being AHEAD of GitHub took the
loud-failure path. That is the normal state after every native push, and the
push-mirror is sync_on_commit with an 8h floor, so one commit can produce up
to 48 false alarms ten minutes apart.
The cost is not the noise. A job that is always red cannot report the one
thing it exists to report, and this job exists to refuse to force-push a real
divergence. The loud failure is only worth keeping if it is rare.
Convergence, not invention: app, git, hanzo.ai, iam and now id already carry
exactly this two-direction check. This repo held the stale copy.
Exercised against real git ancestry -- equal and github-ahead unchanged,
native-ahead DIVERGED(1) -> native-ahead(0), and true divergence still
exits 1. One row moves; the alarm survives.
Co-authored-by: Hanzo Dev <[email protected]>
`POST /v1/billing/gpu-charge` and `GET /v1/billing/gpu-eligibility` answer 404
at api.hanzo.ai — the address and its `-zzq9` nonsense sibling alike, which is
the only way absence is ever shown — and hanzoai/cloud has deleted the
handlers. The client still offered both, so the one outcome either method had
was a 404.
They survived in `pkg/hanzoai/api` + `pkg/hanzoai/models`, the tree the retired
driver wrote. `generate.py` does not own it: sdks.yaml declares exactly one take
path, `hanzoai/cloud -> pkg/hanzoai/cloud`. Nothing regenerates these files, so
nothing would ever have removed them and no future regen can put them back.
Six operations, three models, three exports. `pkg/hanzoai/cloud` is deliberately
untouched — it carries `/v1/billing/gpu/{charge,eligibility}`, which the
deployment still answers for (401, against a 404 control), and publishing an API
smaller than the one served is the same class of lie in the other direction.
generate.py python --check [python] clean
import hanzoai 2208 exports, 0 dangling
cloud-client / duplicate-fields pass, 2362 modules
Co-authored-by: Hanzo Dev <[email protected]>
Mirrors hanzoai/mcp's Rust `lsp` exactly: one tool, two planes, picked by data
the caller already has. `file` names the file; `repo` (a git.hanzo.ai slug,
optional `rev`) says which world it lives in. Without `repo` nothing changes —
the same language server on the same tree. With it, the question goes to the
indexed corpus behind /v1/code/lsp, which reaches across a repo's dependencies
without checking anything out.
One table maps actions onto ops: locate carries a relation (definition|
reference|type|implementation) because "where is X" is one question with four
answers, not four routes; hover, symbols, diagnostics and complete stand on
their own. The body is {repo, rev?, path, line, character, relation?} in LSP's
own frame — 0-based line, UTF-16 character — shifted from the tool's 1-based
`line` by the same expression the local plane uses.
The call goes through the shared HanzoCloud client hanzo-tools-code and
hanzo-tools-net already compose, so there is no second client and no second
lsp tool. Actions a plane cannot serve say so up front rather than spawning a
server or calling out.
test_lsp_tools.py asked for a class named `LspTool`; the class is `LSPTool`,
so those two never ran. One name, and they run.
Co-authored-by: Hanzo Dev <[email protected]>
Temporary. O11yGettableAgentCheckIn declares integration_config and removed_at
twice each, exactly as 3.2.0 shipped them. duplicate-fields must go red on the
forge; cloud-client will stay green, which is the point. Reverted immediately
after.
Co-authored-by: Hanzo Dev <[email protected]>