Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "hanzo-cli"
version = "8.5.753"
version = "8.5.754"
edition = "2021"
authors = ["Hanzo AI"]
description = "Unified CLI for Hanzo AI development tools"
Expand Down
2 changes: 1 addition & 1 deletion E2E.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ Test the public installer and its actual downloaded binaries:
HANZO_E2E_TOKEN="$(hanzo auth token)" make e2e-installed
```

The installer runs in a temporary directory, verifies release checksums, and installs `hanzo`, `hanzo-node`, `dev`, `gen`, and MCP. Tests use that directory first on PATH. It leaves your normal installation intact. A missing token is an error, and every coding test must actually complete; HTTP errors and a missing output file fail the run.
The installer runs in a temporary directory, verifies release checksums, and installs `hanzo`, `hanzo-cli`, `dev`, `gen`, and MCP. Tests use that directory first on PATH. It leaves your normal installation intact. A missing token is an error, and every coding test must actually complete; HTTP errors and a missing output file fail the run.

The tests cover the machine and platform on which they run. They do not replace macOS/Windows installation checks or interactive browser OAuth testing. Token login uses a real existing IAM session; it does not simulate a fresh browser sign-in.
18 changes: 9 additions & 9 deletions LLM.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ the CLI; discovery/marketing repos link here, never copy it.

**Install / run**:
- `curl -fsSL https://hanzo.sh | sh` — the ONE published install line, on every surface.
hanzo.sh drives THIS repo's `install.sh` once per tool (`hanzo`, `hanzo-mcp`, `dev`);
hanzo.sh drives THIS repo's `install.sh` once per tool (`hanzo`, `hanzo-mcp`, `dev`, `gen`, `zt`);
`install.sh` is the one implementation of "fetch a Hanzo binary" (per-platform asset,
sha256-verified, second-name symlink). No token: the repo is public. A tool whose
releases are attested names its workflow (`HANZO_INSTALL_SIGNER`, hanzo.sh's fifth
Expand Down Expand Up @@ -41,20 +41,20 @@ to `crates/client`, so one copy links.
**This binary answers to TWO names, and that is by design.** cloud's Go control
binary (`hanzoai/cloud`, `cmd/hanzo`) owns a small set of control verbs and
delegates every other verb to US, through `cloud/cli/link.go` `fabricCLI()`,
which resolves `hanzo-node` on PATH first and only then `hanzo` (with a
which resolves `hanzo-cli` on PATH first and only then `hanzo` (with a
self-exec guard). So a machine carrying the Go control binary installs THIS
artifact as `hanzo-node`. Same build, two names — there is no third CLI
codebase, and `hanzo-node` is not a separate product.
artifact as `hanzo-cli`. Same build, two names — there is no third CLI
codebase, and `hanzo-cli` is not a separate product.

The trap that follows from it: a stale `hanzo-node` is INVISIBLE. The user types
The trap that follows from it: a stale `hanzo-cli` is INVISIBLE. The user types
`hanzo`, the Go binary delegates, and they get whatever old build is sitting
under the other name — with no version in sight. That is not hypothetical: a
v1.7.2 `hanzo-node` served roughly 150 phantom `/v1/cloud/<verb-noun>` commands,
v1.7.2 `hanzo-cli` served roughly 150 phantom `/v1/cloud/<verb-noun>` commands,
because it predates `genspec.rs` and the registry refutation seam and was built
from the vendored `spec/products.json` (since deleted). Nothing was wrong with
the live code; the INSTALL was old. So:

> Whatever installs `hanzo` MUST install `hanzo-node` at the same version, and
> Whatever installs `hanzo` MUST install `hanzo-cli` at the same version, and
> both must be this crate. An installer that ships one name, or ships two
> different versions, produces a CLI whose surface nobody can explain.

Expand Down Expand Up @@ -82,13 +82,13 @@ parser reads (first line, last token, optional `v` stripped) and the shape every
other `--version` prints. A skew report is a WARNING, not the answer, so it goes
to STDERR and a caller piping the version gets exactly one clean line.

`install.sh` does exactly that: it installs `hanzo`, symlinks `hanzo-node` to
`install.sh` does exactly that: it installs `hanzo`, symlinks `hanzo-cli` to
the same file (a copy where links are unavailable), and warns when another
`hanzo` earlier on PATH would run instead of the build it just placed — the
precedence that let a stale twin hide.

The remaining gaps are elsewhere:
- PyPI carries `hanzo`, `hanzo-cli` and `hanzo-node` as SEPARATE projects that
- PyPI carries `hanzo`, `hanzo-cli` and `hanzo-cli` as SEPARATE projects that
are not this crate. `curl hanzo.sh | sh` used to install one of them at the
name `hanzo`, into `~/.local/bin`, which outranks `/usr/local/bin` on a normal
PATH. It now calls THIS installer instead, so there is one download path and
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ verify: ## Refuse a tree the committed evidence contradicts. Hermetic. The drift
# because stopping at the first failing package is how they stayed unrun: the
# drift gate is upstream of them and fails on real drift by design.
test: ## Everything, including the derivation gates in tests/ — `verify` among them.
@sh scripts/test-installer-strict.sh
@python3 -m unittest discover -s tests -p test_installer.py
@cargo build --quiet --features maintainer --bin driftgate --bin genproduct --locked
@cargo test --workspace --locked --no-fail-fast
Expand Down
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,21 @@ curl -fsSL https://hanzo.sh | sh
One install line, and it is the same one every Hanzo surface prints. It resolves the
release asset for your platform (linux · macOS on amd64 · arm64, Windows on amd64),
verifies its sha256, and puts `hanzo` in `~/.local/bin` — along with `hanzo-mcp` (the
MCP server) and `dev` (the coding agent `hanzo dev` runs). Re-run to upgrade.
MCP server), `hanzo-dev`, `hanzo-gen`, and `hanzo-zet`. The short names `dev`,
`gen`, `mcp`, and `zt` remain available. Re-run to upgrade.

These are prebuilt native binaries; the installer does not compile crates. Node and
Bot have public source but no downloadable native releases yet, so they are not
included. `hanzo-node` is reserved for the network node, not an alias for the CLI.

A provenance warning means the tool installed but its provenance was not verified.
Use `curl -fsSL https://hanzo.sh | sh -s -- --strict` to require provenance verification
before installation (requires a current, authenticated GitHub CLI and release
attestations). Checksums are required in either mode.

Run `hanzo doctor` to check executable paths, versions, authentication and GPU
runtimes. `--offline` skips network checks; `--verify` downloads current releases
and compares installed bytes against checksummed, provenance-verified artifacts.

`hanzo.sh` drives this repo's `install.sh`, which is the one implementation of
"fetch a Hanzo binary". To install only the CLI: `curl -fsSL https://hanzo.sh | sh -s -- hanzo`.
Expand Down Expand Up @@ -179,7 +193,7 @@ hanzo link dial <service> <port> [--install] # a local port onto a service on
hanzo link status | rm <name> # what is up here; unpublish
```

`hanzo-node` is not a second program: the installer writes it as a symlink to this same
`hanzo-cli` is not a second program: the installer writes it as a symlink to this same
binary, because cloud's Go control binary resolves that name first and delegates to it.
One build, two names, so the two can never be different versions.

Expand Down
58 changes: 41 additions & 17 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
#
# HANZO_INSTALL_REPO owning repo (default hanzoai/cli)
# HANZO_INSTALL_BIN binary + asset prefix (default hanzo)
# HANZO_INSTALL_ALIAS second name, same build (default hanzo-node; "" = none)
# HANZO_INSTALL_ALIAS second name, same build (default hanzo-cli; "" = none)
# HANZO_INSTALL_PREFIX install dir (default ~/.local/bin)
# HANZO_VERSION pin a tag (default: highest stable semver)
# HANZO_INSTALL_SIGNER the workflow that must have built the asset, for a tool
Expand All @@ -31,9 +31,10 @@ REPO="${HANZO_INSTALL_REPO:-hanzoai/cli}"
BIN="${HANZO_INSTALL_BIN:-hanzo}"
# The second name this same build installs under: what cloud's control binary
# delegates to. One build, two names — never two versions.
DELEGATE="${HANZO_INSTALL_ALIAS-hanzo-node}"
DELEGATE="${HANZO_INSTALL_ALIAS-hanzo-cli}"
PREFIX="${HANZO_INSTALL_PREFIX:-$HOME/.local/bin}"
SIGNER="${HANZO_INSTALL_SIGNER:-}"
STRICT="${HANZO_INSTALL_STRICT:-0}"

die() { printf '\n%s: %s\n' "$BIN" "$1" >&2; exit 1; }
need() { command -v "$1" >/dev/null 2>&1 || die "need $1 on PATH"; }
Expand All @@ -42,6 +43,23 @@ need curl
need tar
need awk

case "$STRICT" in 0|1) ;; *) die "HANZO_INSTALL_STRICT must be 0 or 1" ;; esac
# Check the verifier before downloading. Strict mode requires repository- and
# tag-bound provenance for every tool, including those without a pinned workflow.
unchecked=""
if [ -n "$SIGNER" ] || [ "$STRICT" = 1 ]; then
if ! command -v gh >/dev/null 2>&1; then
unchecked="needs gh"
elif ! gh auth status >/dev/null 2>&1; then
unchecked="needs a signed-in gh"
elif ! gh attestation verify --help 2>/dev/null | grep -q -- '--source-ref'; then
unchecked="$(gh --version 2>/dev/null | head -1 || true) cannot check an attestation's tag"
fi
if [ "$STRICT" = 1 ] && [ -n "$unchecked" ]; then
die "strict verification unavailable ($unchecked). Install or upgrade GitHub CLI: https://cli.github.com/ and run gh auth login. Nothing was installed."
fi
fi

# A private repo answers 404 to an anonymous asset fetch, so carry a token when
# one is available. Public installs need none.
TOKEN="${HANZO_INSTALL_TOKEN:-${GH_TOKEN:-${GITHUB_TOKEN:-}}}"
Expand Down Expand Up @@ -209,25 +227,22 @@ fetch "$asset.sha256" "$tmp/$asset.sha256" \
# verify an attestation's tag is here, and a verification that ran and failed
# refuses the install, with gh's reason. Otherwise it is not checked, and the
# line says why and how to check it — never a silent pass.
if [ -n "$SIGNER" ]; then
if ! command -v gh >/dev/null 2>&1; then
unchecked="needs gh"
elif ! gh auth status >/dev/null 2>&1; then
unchecked="needs a signed-in gh"
elif ! gh attestation verify --help 2>/dev/null | grep -q -- '--source-ref'; then
unchecked="$(gh --version 2>/dev/null | head -1 || true) cannot check an attestation's tag"
else
unchecked=""
fi
if [ -n "$SIGNER" ] || [ "$STRICT" = 1 ]; then
if [ -z "$unchecked" ]; then
gh attestation verify "$tmp/$asset" --repo "$REPO" --signer-workflow "$REPO/$SIGNER" \
--source-ref "refs/tags/$TAG" > "$tmp/provenance" 2>&1 \
set -- gh attestation verify "$tmp/$asset" --repo "$REPO" --source-ref "refs/tags/$TAG"
if [ -n "$SIGNER" ]; then set -- "$@" --signer-workflow "$REPO/$SIGNER"; fi
"$@" > "$tmp/provenance" 2>&1 \
|| { sed 's/^/ /' "$tmp/provenance" >&2
die "$asset has no attestation that $REPO/$SIGNER built it at $TAG — refusing to install"; }
printf '%s: provenance: %s built %s at %s\n' "$BIN" "$REPO/$SIGNER" "$asset" "$TAG"
die "$asset has no matching provenance for $REPO at $TAG — refusing to install"; }
if [ -n "$SIGNER" ]; then
printf '%s: provenance: %s/%s built %s at %s\n' "$BIN" "$REPO" "$SIGNER" "$asset" "$TAG"
else
printf '%s: provenance: verified %s from %s at %s\n' "$BIN" "$asset" "$REPO" "$TAG"
fi
else
printf '%s: provenance not checked (%s): gh attestation verify %s --repo %s --signer-workflow %s/%s --source-ref refs/tags/%s\n' \
"$BIN" "$unchecked" "$asset" "$REPO" "$REPO" "$SIGNER" "$TAG"
printf '%s: provenance: upgrade GitHub CLI at https://cli.github.com/, run gh auth login, then rerun this installer.\n' "$BIN"
fi
fi

Expand All @@ -241,7 +256,7 @@ chmod 755 "$PREFIX/$BIN$ext"
printf '%s: installed %s\n' "$BIN" "$PREFIX/$BIN$ext"

# The SAME build under a second name. cloud's control binary hands every verb it
# does not own to `hanzo-node` (it resolves that name before `hanzo`), so the two
# does not own to `hanzo-cli` (it resolves that name before `hanzo`), so the two
# names must never be two versions: install one and not the other, or upgrade one
# and not the other, and a user types `hanzo`, gets delegated, and runs an old
# build with no version anywhere on screen. That is not theoretical — a stale
Expand All @@ -261,6 +276,15 @@ if [ -n "$DELEGATE" ]; then
printf '%s: installed %s (the same build, second name)\n' "$BIN" "$PREFIX/$DELEGATE$ext"
fi

# Retire only the symlink this installer used to create for the CLI. A real
# network-node executable (or a link to hanzod) is never removed.
if [ "$BIN" = hanzo ] && [ "$DELEGATE" = hanzo-cli ] && [ -L "$PREFIX/hanzo-node$ext" ]; then
old_target=$(readlink "$PREFIX/hanzo-node$ext" || true)
case "$old_target" in
"hanzo$ext"|"$PREFIX/hanzo$ext") rm "$PREFIX/hanzo-node$ext" ;;
esac
fi

# Whatever PATH says, name the copy that would ACTUALLY run. An earlier entry
# wins, and that precedence is exactly how a stale install hides: the user
# upgrades here and keeps running something else, and re-running the installer
Expand Down
4 changes: 2 additions & 2 deletions scripts/e2e-installed.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ e2e_dir=$(mktemp -d)
trap 'rm -rf "$e2e_dir"' EXIT HUP INT TERM
curl -fsSL https://hanzo.sh -o "$e2e_dir/install.sh"
sh "$e2e_dir/install.sh" --dir "$e2e_dir/bin" hanzo dev gen mcp
for tool in hanzo hanzo-node dev gen hanzo-mcp; do
for tool in hanzo hanzo-cli dev gen hanzo-mcp; do
"$e2e_dir/bin/$tool" --version
done
[ "$("$e2e_dir/bin/hanzo" --version)" = "$("$e2e_dir/bin/hanzo-node" --version)" ]
[ "$("$e2e_dir/bin/hanzo" --version)" = "$("$e2e_dir/bin/hanzo-cli" --version)" ]
PATH="$e2e_dir/bin:$PATH" HANZO_E2E_BIN="$e2e_dir/bin/hanzo"
export PATH HANZO_E2E_BIN
cargo test --locked --test production -- --ignored --nocapture --test-threads=1
64 changes: 64 additions & 0 deletions scripts/test-installer-strict.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/bin/sh
set -eu
root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
fixture=$(mktemp -d)
trap 'rm -rf "$fixture"' EXIT HUP INT TERM
mkdir -p "$fixture/tools" "$fixture/archive" "$fixture/installed"
printf '#!/bin/sh\necho hanzo 1.2.3\n' > "$fixture/archive/hanzo"
tar -czf "$fixture/hanzo-linux-amd64.tar.gz" -C "$fixture/archive" hanzo
(cd "$fixture" && sha256sum hanzo-linux-amd64.tar.gz > hanzo-linux-amd64.tar.gz.sha256)
cat > "$fixture/tools/uname" <<'SH'
#!/bin/sh
case "$1" in -s) echo Linux;; *) echo x86_64;; esac
SH
cat > "$fixture/tools/gh" <<'SH'
#!/bin/sh
case "$*" in
'auth status') exit 0 ;;
'auth token') exit 1 ;;
'--version') echo 'gh version 2.45.0'; exit 0 ;;
'attestation verify --help')
[ "$VERIFY_MODE" = old ] || echo '--source-ref'
exit 0 ;;
*) [ "$VERIFY_MODE" = pass ] ;;
esac
SH
cat > "$fixture/tools/curl" <<'SH'
#!/bin/sh
url=''; dest=''
while [ $# -gt 0 ]; do
case "$1" in
https://*) url=$1; shift ;;
-o) dest=$2; shift 2 ;;
*) shift ;;
esac
done
cp "$INSTALL_FIXTURE/${url##*/}" "$dest"
SH
chmod +x "$fixture/tools/"*
export INSTALL_FIXTURE="$fixture"
export HANZO_INSTALL_TOKEN='' GH_TOKEN='' GITHUB_TOKEN=''
export HANZO_VERSION=v1.2.3 HANZO_INSTALL_REPO=hanzoai/cli HANZO_INSTALL_BIN=hanzo
export HANZO_INSTALL_ALIAS=hanzo-cli HANZO_INSTALL_PREFIX="$fixture/installed"
export PATH="$fixture/tools:$PATH"
for shell in sh bash; do
for mode in old reject; do
printf 'previous installation\n' > "$fixture/installed/hanzo"
if VERIFY_MODE="$mode" HANZO_INSTALL_STRICT=1 "$shell" "$root/install.sh" > "$fixture/log" 2>&1; then
echo "strict $mode unexpectedly succeeded" >&2; exit 1
fi
grep -q 'previous installation' "$fixture/installed/hanzo"
done
ln -sf hanzo "$fixture/installed/hanzo-node"
VERIFY_MODE=pass HANZO_INSTALL_STRICT=1 "$shell" "$root/install.sh" > "$fixture/log" 2>&1
"$fixture/installed/hanzo-cli" --version | grep -q 'hanzo 1.2.3'
[ ! -L "$fixture/installed/hanzo-node" ]
printf 'network node\n' > "$fixture/installed/hanzo-node"
VERIFY_MODE=old HANZO_INSTALL_STRICT=0 HANZO_INSTALL_SIGNER=.github/workflows/release.yml \
"$shell" "$root/install.sh" > "$fixture/log" 2>&1
grep -q 'provenance not checked' "$fixture/log"
grep -q 'upgrade GitHub CLI' "$fixture/log"
grep -q 'network node' "$fixture/installed/hanzo-node"
rm "$fixture/installed/hanzo-node"
echo "$shell: strict verification and preservation of existing installs passed"
done
Loading
Loading