Skip to content
Draft
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
37 changes: 37 additions & 0 deletions CLOUD-PARITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Cloud coding session acceptance

Target: the cloud-session workflows in the supplied Claude Code field guide.
An API route alone does not count as end-to-end support. Pending means not yet
verified in production, including capabilities already present in the service.

| Workflow | Evidence / remaining acceptance |
| --- | --- |
| Public installation and authenticated local coding | v8.5.753 installer and three production E2Es passed |
| Independent cloud coding run | Production run completed after terminal detached |
| Commit, authenticated push and PR | Production smoke PRs #6 and #7 in hanzoai/cli; closed after verification |
| Follow-up after completion | Production follow-up read preceding run's committed file |
| Branch checkout from cloud | Production teleport fetched exact committed file |
| Parallel isolated machines and ports | Pending concurrent production acceptance |
| In-flight instructions | In progress: public caller-scoped steering and CLI handoff tracking |
| Queued instructions and retraction | Pending; interrupt-and-follow-up is not a retractable queue |
| Conversation continuation locally | Pending; branch checkout alone does not restore model context |
| Local repository bundle, including non-GitHub repositories | Pending |
| Plan / build / setup modes | Existing CLI selectors; pending full production acceptance |
| Environment selection, scripts, variables and services | Existing service; pending usable CLI workflow and acceptance |
| Network policies and external credential injection | Pending isolation and denied-host acceptance |
| Repository instructions, skills, hooks and MCP | Pending parity audit and production acceptance |
| GitHub personal / org onboarding and useful auth errors | Connected account verified; pending missing-App and SSO cases |
| CI and PR review auto-fix | Pending trigger-to-fix production acceptance |
| Scheduled, HTTP and GitHub triggered runs | Pending coding-specific workflow and acceptance |
| Coordinated projects | Pending |
| Browser, phone and desktop start / follow / steer | Pending browser workflow acceptance |
| Slack start / follow / steer | Existing integration tests; pending production acceptance |
| Diff review, inline comments and draft PRs | Pending |
| Share, archive and delete sessions | Pending ownership and visibility acceptance |
| Idle recovery and durable conversation | Pending restart/reclaim acceptance |
| Self-hosted execution and private networks | Pending |
| Usage, concurrency and clear billing errors | 402 identity fix deployed; pending limits acceptance |

Do not describe full parity as released until the pending rows have concrete
evidence. Keep release checks in E2E.md and the opt-in production tests; avoid
creating PRs, spending indefinitely, or modifying user repositories in default tests.
21 changes: 16 additions & 5 deletions CLOUD-SESSIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,25 @@ accounts are checked by the service. `--model` names a model for harnesses that
support it. `--mode plan` reads without pushing; `--mode setup` inspects the
repository's environment. The normal mode is `build`.

`--target MACHINE_ID` selects a registered machine you own for a build run.
`--timeout-seconds 600` bounds the execution budget (1–1800 seconds), and
`--effort low|medium|high` selects reasoning effort for supported harnesses.
Steering a running session preserves its configuration; start a new task to
change its repository, harness, model, mode or execution settings.

Without `--repo`, the CLI reads origin and the current branch, refuses known
unpushed commits or a missing upstream, and warns about uncommitted files.
Only pushed files reach the sandbox. GitHub and Hanzo forge origins are supported;
there is no local repository bundle upload. An explicit `--repo` targets that
remote directly and does not upload or inspect the local checkout.

Follow-up tasks require a finished or paused run. They use the service's `after`
field to continue from prior work on a new branch. This CLI does not currently
redirect a running cloud sandbox. Ctrl-C while watching only detaches the viewer.
Follow-up tasks on finished runs use the service's `after` field to continue
from prior work on a new branch. On running or paused sessions, the new instruction
redirects the run: it keeps its current work, then starts a follow-up. Add `--watch`
to wait for that handoff and follow the new session. To reconnect during the
handoff, run `hanzo --cloud --resume SESSION_ID --await-followup`. This is an
interrupt-and-continue operation; retractable message queues are still pending.
Ctrl-C while watching only detaches the viewer.
The terminal shows recent events; the browser session holds the transcript.

`--teleport` checks the repository identity and clean working tree, fetches the
Expand Down Expand Up @@ -79,8 +89,9 @@ local dev task that writes and verifies a file also passed against production.
See [E2E.md](E2E.md) for repeatable source and public-installer checks.

Still unimplemented here: uploading a local repository bundle, importing the
native harness conversation into a local session, and delivering follow-up
instructions to a running cloud sandbox. Scheduling, environment configuration,
native harness conversation into a local session, and retractable queued messages.
The new steering workflow requires the matching API release and production
verification before being counted as shipped. Scheduling, environment configuration,
pause/stop, PR creation/merge, browser/mobile handoff, and CI auto-fix were not
verified end to end in this change. Published API routes alone do not prove those
workflows.
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
8 changes: 7 additions & 1 deletion E2E.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,18 @@ HANZO_E2E_TOKEN="$(hanzo auth token)" make e2e

This exercises token login through stdin, identity, authenticated usage JSON, a local `dev` task that writes and verifies a file in a temporary Git repository, and a read-only cloud task that clones a real repository and completes inference. It requires `dev` on PATH. The local and cloud tasks consume normal usage; the cloud test leases a sandbox. It does not log out your existing account or modify the source repository.

The steering check also starts a bounded read-only task, sends an instruction
while it runs, follows the replacement session, and verifies its repository
answer. It requires an API release serving `/v1/agent/coding/{session}/steer`.
Both sessions have a 120-second execution budget. The test fails if the
instruction is refused or the handoff never completes.

Test the public installer and its actual downloaded binaries:

```sh
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
Loading
Loading