CLAWDBOT_INSTALL_URL/CLI_URL targeted hanzo-bot.ai, which does not resolve,
so the installer docker test died on 'curl: (6) Could not resolve host'.
The installer is served from hanzo.bot (install.sh + install-cli.sh both
200). Retarget both to hanzo.bot.
handleIamOAuthHttpRequest (/auth/login, /auth/callback, /auth/refresh,
/auth/logout, /auth/userinfo) was exported but never added to the request
stage list, so /auth/login returned 404 and the browser login flow to
hanzo.id was unreachable. Add it as the first, public stage; the handler
self-guards on the /auth prefix and only activates in gateway.auth.mode
== iam, so it never shadows a downstream route.
The root Dockerfile asserts dist/control-ui/index.html exists so a release
image never ships without the admin-bot SPA. The install smoke builds the
same Dockerfile without the private admin bundle in context and only
exercises the hanzo-bot CLI, so it tripped that guard and failed red.
Gate the assertion behind REQUIRE_ADMIN_UI (default 1 keeps release builds
loud); the CLI smoke builds with REQUIRE_ADMIN_UI=0.
The self-hosted runner pool serves pull_request jobs for hanzoai/bot but is
not offering it push/workflow_dispatch jobs (they queue with no runner). Add
a scoped pull_request trigger (Dockerfile + this workflow) so the vendored
build can run and publish an image through a working, sanctioned lane.
hanzoai/bot is public and hanzoai/.github (home of the docker-build.yml
reusable workflow) is private; GitHub rejects a public caller of a private
reusable workflow at parse time (422 startup_failure, 0 jobs) so no image
ever shipped. Vendor the amd64 build/push inline on the same self-hosted
arcd pool. Also fix the admin SPA prebuild to clone hanzoai/admin
(apps/admin-bot == @hanzo/bot-admin) — the prior gui/apps/admin-bot path
does not exist in hanzoai/gui, so the control-ui assert would fail too.
Strip the home-grown gateway password auth mode end to end so human login
routes only through @hanzo/iam (OIDC) and machine callers use the shared
token. Password was a second shared-secret login path parallel to IAM; it
is now gone from the type unions, config schema, credential resolution,
handshake enforcement, and every consumer.
Server/enforcement:
- auth.ts: drop "password" from ResolvedGatewayAuthMode / mode-source /
GatewayAuthResult.method / ConnectAuth; remove the password authorize
branch, the password mode auto-selection, and the assert branch.
- server-runtime-config.ts: token-only shared-secret check; tailscale
funnel now requires token or iam.
- ws-connection auth-context/auth-messages/message-handler: token-only
handshake, no password provided-kind.
- startup-auth / auth-install-policy / auth-mode-policy: drop password
secret-ref resolution and ambiguity gating (token is the only secret mode).
Config/schema/env:
- types.gateway.ts + zod-schema.ts: remove gateway.auth.password and
gateway.remote.password and "password" from the mode enums.
- protocol snapshot + control-ui-contract: drop the password authMode literal.
- config/io.ts: stop passing BOT_GATEWAY_PASSWORD through.
Client/credentials:
- credentials.ts + call.ts + client.ts + probe.ts: token-only credential
resolution; the client never sends a password in the connect auth.
Consumers (CLI/wizard/tui/audit/pairing/node-host/browser): remove password
menu options, prompts, secret-ref resolution, diagnostics, and setup-code
payload handling; onboarding/configure offer token only.
Production source typechecks with no new errors, lints clean, and the
bundle builds. Password-behavior test cases still reference the removed
API and need a follow-up removal pass.
Remove three exported empty-array backwards-compat constants that have
zero references anywhere in the repo:
- LEGACY_PLUGIN_MANIFEST_FILENAMES
- LEGACY_CANVAS_HANDLER_NAMES
- LEGACY_MACOS_APP_SOURCES_DIRS
The fork carries no legacy names, so these were pure dead indirection.
build:strict-smoke green; legacy-names consumers unaffected.
Red CRITICAL-1 (deploy gate) — cross-tenant secret exfil via the sanctioned
@hanzo code: path. The coding agent no longer inherits the pod env:
- buildAgentEnv: minimal allowlist (PATH/HOME/LANG/LC_*/TERM/TMPDIR/SSL_CERT_*
+ HANZO_CODING_AGENT_ENV_ALLOW). Excludes BOT_GATEWAY_TOKEN, KMS/cloud creds,
every pod secret. The git credential rides env http.extraHeader only.
- createDockerRuntime is the DEFAULT: per-task tmpfs /work + /tmp (nosuid),
--cap-drop ALL, --security-opt no-new-privileges, no SA-token mount, egress
optional/off. If the container runtime is unavailable it FAILS CLOSED (503),
never falls back to the host.
Proven by an exfil-negative test: a fake "dev" that dumps env + reads the SA
token and commits it -> the pushed branch contains PATH but neither the gateway
bearer, KMS key, nor the git credential.
Red HIGH-2: auth mode must be token or iam; mode 'none' is rejected 503.
Red MEDIUM-4: pin the git clone/push host (HANZO_CODING_GIT_HOSTS, default
git.hanzo.ai/api.hanzo.ai) and add -c http.followRedirects=false to clone AND
push so a redirect can't smuggle the credential to another host.
Red MEDIUM-5: global(8)/per-org(2) concurrency semaphore -> 429 over cap.
Red LOW-7: BRANCH_RE constrained to ^agent/[a-f0-9]{6,64}$.
- coding-task.test.ts: clone->edit->commit->push against a REAL local bare
git repo with a stub agent; asserts branch lands, credential never on
argv, no-change path skips push, agent failure => no commit/push.
(Proven end-to-end via node --experimental-strip-types smoke; full
vitest run is gated on the private-registry pnpm install.)
- coding-tasks-http.test.ts: full handler over a stub runtime — NDJSON
result stream, workdir always disposed, and the isolation guards:
clone-URL org must match X-Org-Id, https-only, safe branch, credential
required, tenant required, 405 for non-POST.
bot-gateway side of the @hanzo agent keystone. clone a native /v1/git
repo, run the coding agent (dev exec --sandbox workspace-write --json),
commit on a fresh branch, push it back with the agent credential; stream
NDJSON step/log/result so cloud mirrors it into the agent session.
- coding-task.ts: sandbox-agnostic runCodingTask over an injected ExecFn
(host now; docker exec swap = per-task isolation follow-on). Credential
via env http.extraHeader only — never argv/URL/logs. No force-push/delete.
- coding-tasks-http.ts: the gateway stage. Pod-boundary auth first, then
the clone URL org segment must match the minted X-Org-Id (isolation).
- server-http.ts: registered the coding-tasks stage after tools-invoke.
Tests next (runner vs local bare repo + handler stream).
The public repo could not be installed or run, and the two defects share a
cause: a rename left constants pointing at names that do not exist.
Install: @hanzo/iam was pinned to github:hanzo-js/iam#main, a private repo, so
pnpm install failed with ERR_PNPM_FETCH_404 for anyone without access to it —
including CI, which passes no token, so the image install could not have
resolved it either. The package is published, so depend on it from npm. That
leaves no dependency needing credentials, so the Dockerfile's GIT_TOKEN plumbing
goes too; it wrote the token into .npmrc and took it on a build arg, which
docker history keeps.
The SDK names its server-side types Config/AuthResult/JwtClaims, so alias them
to the Iam* names the gateway wrapper exposes. Config is what validateToken and
IamClient accept; IAMConfig is the browser entrypoint's config and does not
belong here.
CLI: package.json declares hanzo-bot.mjs as its bin, but ENTRY_WRAPPER_PAIRS
listed bot.mjs twice and hanzo-bot.mjs never, so isMainModule() saw no pair,
entry.ts skipped its body, and every invocation exited 0 with no output — a
gateway that starts, registers nothing and stops. Derive the pairs from the
bin wrappers and the entry files hanzo-bot.mjs imports, as a cross product, so
a row cannot be duplicated or dropped.
Version: resolveBinaryVersion() skips any package.json whose name is not
CORE_PACKAGE_NAME, which read "bot" while the package ships as @hanzo/bot, so
the package.json path resolved nothing and the version came from build-info.json
alone — absent from a bundle-only build, which then reported 0.0.0.
Both new tests fail when their defect is reintroduced: they assert against the
bins package.json declares and against the real package.json, rather than
restating the value under test.
Also from hanzobot/core: cloud-node detection behind one helper, node-host
reusing startPlaygroundRegistration instead of a second hand-rolled copy, the
playground node API on /v1, and the install smoke tests naming the package
@hanzo/bot rather than the bin name.
RED review found the bots list/stop stage was the lone data-plane endpoint on the
gateway with NO auth — it resolved the tenant purely from the client-controllable
X-Org-Id (getIamIdentity, pure-trust). Every sibling (tools-invoke, chat-bridge,
llm-proxy) gates on resolvedAuth. Since this pod is reachable off-gateway
(in-cluster / SSRF), an attacker could list AND destructively stop any org's runs
with a forged X-Org-Id and no credential.
- Add the authorizeHttpGatewayConnect gate to the bots stage (parity with
tools-invoke): auth runs BEFORE the identity read, so the forwarded X-Org-Id is
only trusted once the caller is proven authorized. server-http wires resolvedAuth
+ trustedProxies + rateLimiter into the stage like its siblings.
- stopOrgBot: ownership check via Object.prototype.hasOwnProperty.call(store,runId)
instead of `store[runId]`, so a prototype-member runId (__proto__/constructor/…)
can't yield a false "stopped" for a run that never existed.
Belt-and-suspenders: a NetworkPolicy restricting bot-gateway ingress to the gateway
+ cloud-api should still be applied (ops). Hand-verified against the tools-invoke
pattern; no local typecheck (no CI / auth-gated dep in local install).
Minimal list+stop surface the Hanzo Cloud control plane proxies for the
hanzo.bot console. Pure-trust identity headers (getIamIdentity) scope both to
the caller's org; isolation is the tenant path -- a run lives in that org's
session store under ~/.bot/tenants/{orgId}/agents/{agentId}/sessions/, so one
org can never see or stop another's runs.
- GET /v1/bots lists the org's run sessions as
{runId,task,surface,status,startedAt} (sessionUrl is derived control-plane
side from runId). No org header => empty list.
- POST /v1/bots/:runId/stop removes the run from the org's store and returns
{runId,status:stopped}; a run the org does not own is 404.
Wired as a request stage in createGatewayHttpServer, next to hooks/tools-invoke,
using the existing sendJson + session-store helpers (no new deps).
main's Dockerfile had no auth for the private codeload tarball dep
(@hanzo/iam = github:hanzo-js/iam), so pnpm install --frozen-lockfile
404'd. Add a GIT_TOKEN build-arg that writes always-auth + host _authToken
to .npmrc (always-auth is required for pnpm to send the token on a
non-registry tarball fetch), then strips it in the same layer so the
token never persists in the single-stage image.
Replaces non-canonical scale-set / org-prefixed labels with the
existing labels every arcd host registers with. Matches evo for
amd64 and spark for arm64. No new labels added.
- src/cli/social-cli.ts: bot social {post,integrations,schedule}
- post: POST /public/agent on social.hanzo.ai (AGENT_API_KEY-gated)
- integrations / schedule: stubbed pending JWT SSO from hanzo.id
(tracked as Task #28 — Hanzo IAM client hanzo-social).
- src/cli/program/register.subclis.ts: register social subcommand.
URL configurable via HANZO_SOCIAL_URL (default https://social.hanzo.ai).
Agent key via HANZO_SOCIAL_AGENT_API_KEY — provisioned by KMS to bot pod.
The cluster image pinned at v2026.3.26-amd64 was built before the
openclaw.plugin.json -> bot.plugin.json migration landed (commit
0fa6a33a93 "Eradicate OpenClaw" + 74072491fc identifier sweep), so
bot-gateway pods fail config validation at boot when they cannot
find /app/extensions/{ci-fix-loop,continuous-learning,flow,
google-antigravity-auth,self-improvement}/openclaw.plugin.json
(all 5 ship bot.plugin.json on HEAD).
Cuts a fresh semver tag so CI republishes ghcr.io/hanzoai/bot
with the new manifest filename and the post-rebrand control-ui /
gateway code, unblocking the bot-gateway 0/1 Deployment.
Strict cross-org isolation in the shared docker-build.yml workflow
means hanzobot repos cannot dispatch to hanzo-build-linux-amd64
(hanzoai org pool). Provisioned hanzobot-build-linux-amd64 ARC pool
in hanzoai/universe#89; this PR routes builds to it.
Adds pnpm overrides for 31 high / 3 critical advisories that the
pnpm-audit-prod pre-commit hook surfaces (CI was failing).
Key bumps: hono >=4.12.18, axios >=1.15.2, protobufjs >=8.0.2,
fast-xml-parser >=5.7.0, @opentelemetry/sdk-node >=0.217.0,
dompurify >=3.4.0, file-type >=21.3.1, ip-address >=10.1.1,
postcss >=8.5.10, vite >=7.3.2, yaml >=2.8.3, lodash >=4.18.0
and several others.
Also: extensions/brain and extensions/memory had '@hanzo/bot-graph-links: "*"'
which pnpm 10 no longer auto-resolves to a workspace package without an
existing lockfile entry. Switched to explicit 'workspace:*' so a clean
lockfile regen works.
Audit: 85 vulns (3 critical / 31 high / 49 moderate / 2 low) → 12 vulns
(0 critical / 0 high / 11 moderate / 1 low). At --audit-level=high CI
hook now passes (exit 0).
The big rebrand left a residue of HanzoBot* identifiers inside camelCase
compounds (resolveHanzoBotAgentDir, HanzoBotPackageRoot, hanzoBotRoot,
HanzoBotTmpDir, …) that the canonical bot-root.ts / tmp-bot-dir.ts had
always exported as Bot* / bot*. The earlier \b-anchored sweep missed
them because the H is mid-identifier with no word boundary. Replace
HanzoBot(?=[A-Z][a-z]) → Bot (and hanzoBot equivalent) everywhere, plus
HanzoBotKit → BotKit (the on-disk module is apps/shared/BotKit).
Also one stray pi-tools.ts type narrowing (senderId is string|null but
the target wants string|undefined) — coerce with ?? undefined.
pnpm build → ✔ Build complete in 7s.
Decomplecting the publish surface: there's one canonical TS runtime
and one meta. They had been sharing the @hanzo/bot npm name, which
made the meta at hanzoai/bot uninstallable.
hanzobot/ts → @hanzobot/ts (this repo — the canonical impl)
hanzoai/bot → @hanzo/bot (meta; depends on @hanzobot/ts)
Touches root package.json plus 11 workspace package.json files that
declared @hanzo/bot@workspace:* — now they declare @hanzobot/ts.
CLI bin stays 'hanzo-bot' (user-facing surface unchanged).
Three coordinated things:
1. Resolve the committed upstream/main merge conflicts. 94 files were
sitting with raw <<<<<<< HEAD ... >>>>>>> upstream/main blocks. Every
conflict had HanzoBot/Bot on HEAD and OpenClaw on upstream. Resolved
by keeping HEAD everywhere — 540 conflict blocks.
2. Rewrite remaining OpenClaw text refs.
OPENCLAW → HANZO_BOT
OpenClaw → HanzoBot (PascalCase type identifiers)
openclaw → bot (codebase convention — botToken,
botUsername etc.; avoids invalid
hyphenated JS identifiers)
openclaw[_]/openclaw[A-Z] → bot[_]/bot[A-Z] (compound)
ai.openclaw.x → ai.hanzo.bot.x (JVM package path)
3. Delete or rename openclaw-named file paths (116 of them). Dead
duplicates with a Bot-named canonical from the partial migration:
deleted. Otherwise renamed.
ai/openclaw/** → deleted (ai/hanzo/bot/** canonical)
Sources/OpenClaw*/ → deleted (Sources/Bot* canonical)
Tests/OpenClawIPCTests/ → deleted (Tests/BotIPCTests canonical)
OpenClawKit/ → deleted (BotKit canonical)
openclaw-tools.*.ts → deleted (bot-tools.*.ts canonical)
openclaw-root.ts etc. → deleted (bot-root.ts canonical)
types.openclaw.ts → deleted (types.bot.ts canonical)
extensions/*/openclaw.plugin.json → renamed hanzo-bot.plugin.json
docs/start/openclaw.md → renamed hanzo-bot.md (+ zh-CN)
docs/assets/openclaw-*.png, whatsapp-openclaw*.jpg → deleted
scripts/.../openclaw-* → deleted (bot/hanzo-bot parallels)
openclaw.mjs → deleted (hanzo-bot.mjs is package.bin)
Then patched 65 broken JS/TS import strings where hanzo-bot had ended
up inside an import path ('./types.hanzo-bot.js' → './types.bot.js'
etc., since the on-disk filename uses the brand-neutral bot- prefix).
Pre-existing lint debt cleaned up to get oxlint --type-aware to 0/0:
removed the dead i18n test that referenced a path no longer existing;
defined the missing DIDConfig and WalletConfig types in types.base.ts
(they were imported but never declared); dropped unused imports;
collapsed redundant type assertion and a tautological meta.bot lookup
that the openclaw→bot rename made redundant.
The a2ui.bundle.js generated artifact is kept at its pre-rebrand
contents — it gets regenerated by 'pnpm canvas:a2ui:bundle' from
sources I cannot rebuild in this commit; the source side is clean.
Verified: 0 occurrences of openclaw (case-insensitive) in tree source
content, 0 file or directory paths with openclaw in the name; oxlint
--type-aware src test reports 0/0.
The codebase already had 663 BOT_* env-var refs from a partial migration
(BOT_CONFIG_PATH, BOT_SHELL, BOT_GATEWAY_BIND, …). Finish the rename:
every remaining OPENCLAW_ prefix becomes BOT_, unifying the env-var
surface on a single brand-neutral prefix.
3493 occurrences across 684 files (src/, test/, extensions/, apps/*/,
docs/, scripts/, infra configs). Clean break — no backwards-compat
reads. Users with existing OPENCLAW_* exports need to rename those.
Out of scope (separate change):
- the ai.openclaw.android.* Java package (partial migration to
ai.hanzo.bot.android.* already in place; finishing it requires
moving files + AndroidManifest + applicationId + signing)
- __openclaw__/ HTTP path prefix served by the gateway
Strip upstream marketing surfaces (logo picture, EXFOLIATE tagline,
sponsor block pointing at upstream's OpenAI/Vercel/Blacksmith/Convex
relationship) and rewrite refs:
openclaw → hanzo-bot (CLI name from package.bin)
OpenClaw → Hanzo Bot (display name)
openclaw/openclaw → hanzoai/bot
openclaw.ai → hanzo.ai
docs.openclaw.ai → docs.hanzo.ai
discord.gg/clawd → hanzo.ai#community
openclaw/nix-openclaw → hanzoai/nix-bot
Touches the top-level README plus the in-tree READMEs that carried the
same upstream copy (apps/*, assets/chrome-extension, docs/security,
docs/.i18n, extensions/*, scripts/shell-helpers, src/hooks/bundled).
Residual OPENCLAW_* env-var refs are intentionally left — they document
the runtime config keys the code still reads. Renaming those needs a
matching code change with a config migration.
Correct positioning per the actual product hierarchy:
• zapdb is the canonical Hanzo storage layer — ZAP-native, multi-
language, generic. Canonical home is `zap-proto/db`. Currently
mirrored at `luxfi/zapdb` (Go) and `~/work/luxcpp/zapdb` (C++);
both are in-flight to migrate to `zap-proto/db`.
• luxfi/database is the Lux-flavored extension of zapdb — adds
blockchain concerns (chain heads, archival, validator sets) on top
of the base zapdb. NOT a generic brain backend.
Changes:
• ADVERTISED: rename "luxdb" → "zapdb" with the canonical zap-proto/db
pointer.
• Module header doc: list zapdb in the canonical-backend bullets and
clarify luxfi/database is the Lux extension, not the brain backend.
• README table: zapdb gets the canonical row pointing at zap-proto/db;
luxdb gets a separate row labeled as the Lux extension.
• README "native distributed-SQL story" section: ZAP repos under
zap-proto org enumerated (one per language: c, cpp, go, py, rust,
…). zapdb canonical home noted as zap-proto/db.
• Routing table: multi-machine + team rows reference `zapdb`
(zap-proto/db) instead of `luxfi/database`.
Rust crate mod.rs header similarly corrected to point at zapdb /
zap-proto/db as the canonical native store.
We don't use libSQL or Turso. Multi-replica SQL on Hanzo runs on:
• ZAP transport (zero-copy, same protocol the rest of the stack speaks;
natively carries MCP, A2A, and ACP)
• hanzo-consensus (metastable agent agreement, applicable at the
storage write-ordering layer as a quorum primitive)
• luxfi/database (Go public API) + luxfi/zapdb (internal) and the
luxcpp/zapdb C++ port for the same wire format
Changes:
• ADVERTISED: drop "libsql" entry, add "luxdb" pointing at
@hanzo/bot-memory-luxdb (in-flight sibling backed by luxfi/database).
• README: replace "libSQL/Turso embedded replicas" routing line with
"luxfi/database over ZAP + hanzo-consensus (our native distributed-
SQL story; in-flight)".
• README: new section documenting the ZAP + consensus + zapdb stack
end-to-end, plus the OSS license per backend (Apache 2 / BSD-3 / MIT).
We deliberately do not adopt libSQL / Turso — adopting them would force
us off our own ZAP+consensus path. The multi-replica primitive is
in-flight at luxfi/database (Go) and luxcpp/zapdb (C++).
• Path: ~/.hanzo-bot/brain/brain.db → ~/.hanzo/brain/brain.db.
Shared across every Hanzo SDK on a machine (bot, mcp, dev, python).
No more per-product directories — every artifact under one root.
• Pluggable BrainStore now advertises every canonical backend with
install-on-demand messaging. listBackends() shows:
sqlite (in-core default, ships)
qdrant → @hanzo/bot-memory-qdrant (~/work/hanzo/vector)
meilisearch → @hanzo/bot-memory-meilisearch (~/work/hanzo/search)
postgres → @hanzo/bot-memory-postgres (multi-tenant team)
lancedb → @hanzo/bot-memory-lancedb (already shipped)
libsql → @hanzo/bot-memory-libsql (Turso/sqld multi-replica)
replicate → @hanzo/bot-memory-replicate (~/work/hanzo/replicate
SQLite WAL → S3 backup)
vfs → @hanzo/bot-memory-vfs (~/work/hanzo/vfs S3
streaming block FS)
• README docs the scaling decision tree per workload size.
• Bot deploys the same ~/.hanzo paths as hanzo-mcp + python-sdk, so
a brain.db written by one runtime is read by all of them.