auth: the two key shapes are pk- and sk-, and one sentence says so

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]>
This commit is contained in:
zooqueen
2026-08-08 15:04:19 -07:00
co-authored by Hanzo Dev
parent 8974dc7af2
commit 20a2178e61
11 changed files with 34 additions and 19 deletions
+1 -1
View File
@@ -66,7 +66,7 @@ Each reads `HANZO_API_KEY` from the environment and talks to
`https://api.hanzo.ai` unless `HANZO_BASE_URL` says otherwise:
```bash
export HANZO_API_KEY=hk-...
export HANZO_API_KEY=sk-...
uv run python -m examples.hello
```
+1 -1
View File
@@ -32,7 +32,7 @@ def api_key() -> str:
"""
key = os.environ.get("HANZO_API_KEY")
if not key:
raise SystemExit("HANZO_API_KEY is not set — export an IAM JWT or an hk- cloud key")
raise SystemExit("HANZO_API_KEY is not set — export an IAM JWT or a pk-/sk- cloud key")
return key
+3 -3
View File
@@ -20,7 +20,7 @@ pip install hanzo-flags # or: uv add hanzo-flags
```python
from hanzo_flags import HanzoFlags
flags = HanzoFlags("https://api.hanzo.ai", token="hk-...")
flags = HanzoFlags("https://api.hanzo.ai", token="sk-...")
flags.load("user-123", person_properties={"plan": "pro"})
if flags.is_enabled("checkout-exp"):
@@ -46,7 +46,7 @@ Async (asyncio services):
```python
from hanzo_flags import AsyncHanzoFlags
flags = AsyncHanzoFlags("https://api.hanzo.ai", token="hk-...")
flags = AsyncHanzoFlags("https://api.hanzo.ai", token="sk-...")
await flags.load("user-123")
if flags.is_enabled("checkout-exp"):
...
@@ -56,7 +56,7 @@ One-shot:
```python
from hanzo_flags import evaluate
res = evaluate("https://api.hanzo.ai", "user-123", token="hk-...")
res = evaluate("https://api.hanzo.ai", "user-123", token="sk-...")
res.is_enabled("checkout-exp")
```
@@ -25,7 +25,13 @@ from pathlib import Path
from dataclasses import field, dataclass
from urllib.parse import unquote, urlparse
from hanzo_tools.core import BaseTool, CloudError, HanzoCloud, MCPResourceDocument
from hanzo_tools.core import (
NO_KEY,
BaseTool,
CloudError,
HanzoCloud,
MCPResourceDocument,
)
# LSP server configurations
LSP_SERVERS = {
@@ -633,7 +639,7 @@ class LSPTool(BaseTool):
return {
"action": action,
"repo": repo,
"error": "no hk- key: set HANZO_API_KEY or ~/.hanzo/config.json .apiKey",
"error": NO_KEY,
}
# The wire speaks LSP's own frame: 0-based line, 0-based UTF-16
+3 -3
View File
@@ -11,7 +11,7 @@ import json
import asyncio
from hanzo_tools.lsp import TOOLS, LSPTool
from hanzo_tools.core import HanzoCloud
from hanzo_tools.core import NO_KEY, HanzoCloud
from hanzo_tools.lsp.lsp_tool import CLOUD_OPS, LOCAL_ACTIONS
@@ -42,7 +42,7 @@ class FakeClient:
return FakeResponse(self.reply, self.status)
def tool_over(reply=None, key="hk-test", status=200):
def tool_over(reply=None, key="sk-test", status=200):
"""An LSPTool whose cloud client answers from a canned transport."""
tool = LSPTool()
cloud = HanzoCloud(key=key)
@@ -153,7 +153,7 @@ def test_upstream_failure_is_reported_not_raised():
def test_without_a_key_nothing_is_sent():
tool, client = tool_over(key="")
out = _run(tool.run(action="hover", repo="hanzoai/mcp", file="a.py", line=1))
assert "hk-" in out.data["error"]
assert out.data["error"] == NO_KEY
assert client.calls == []
+1 -1
View File
@@ -234,7 +234,7 @@ class FakeStreamClient:
def cloud_over(lines, status=200, body=""):
"""A real HanzoCloud whose transport replays `lines`."""
cloud = HanzoCloud(key="hk-test")
cloud = HanzoCloud(key="sk-test")
client = FakeStreamClient(FakeSSEResponse(lines, status=status, body=body))
async def fake_get_client():
@@ -5,7 +5,7 @@ Uses the Hanzo Cloud RAG pipeline:
- POST /v1/chat-docs — RAG chat with component context
- POST /v1/index-docs — index components into the search backend
Authentication: publishable key (pk-*) for read, API key (hk-*) for write.
Authentication: publishable key (pk-) for read, secret key (sk-) for write.
"""
import logging
@@ -30,6 +30,7 @@ from hanzo_tools.core.unified import (
)
from hanzo_tools.core.id_tool import IdTool, id_tool
from hanzo_tools.core.cloud import (
NO_KEY,
HanzoCloud,
CloudError,
cloud_api_key,
@@ -61,6 +62,7 @@ __all__ = [
"IdTool",
"id_tool",
# Cloud client (api.hanzo.ai)
"NO_KEY",
"HanzoCloud",
"CloudError",
"cloud_api_key",
+12 -5
View File
@@ -5,9 +5,9 @@ search, web search, research, vision — talks to it through this one client.
There is exactly one place that knows the base URL, the auth header, and how to
turn a non-2xx into a typed error; tools compose it, never re-implement it.
Auth resolves in order: ``HANZO_API_KEY`` env, the ``apiKey`` (hk- key) in
``~/.hanzo/config.json``, then the ``hanzo`` CLI's live IAM session. Base URL is
``https://api.hanzo.ai``, overridable via ``HANZO_API_BASE``.
Auth resolves in order: ``HANZO_API_KEY`` env, the ``apiKey`` (a ``pk-``/``sk-``
key) in ``~/.hanzo/config.json``, then the ``hanzo`` CLI's live IAM session.
Base URL is ``https://api.hanzo.ai``, overridable via ``HANZO_API_BASE``.
Reference: HIP-0300 unified tools; api.hanzo.ai /v1 surface.
"""
@@ -21,6 +21,13 @@ from collections.abc import AsyncIterator
DEFAULT_BASE = "https://api.hanzo.ai"
#: What a cloud-backed tool says when no key resolved — one sentence in one
#: place, so the shapes cannot drift apart tool by tool. Cloud admits two:
#: ``pk-`` is the publishable key you may ship in a browser bundle, ``sk-`` is
#: the one you may not. ``APIKeyPrefixes`` in cloud's ``auth_identity.go`` is
#: the authority; anything else resolves to no principal.
NO_KEY = "no API key: run `hanzo login` or set HANZO_API_KEY (pk-/sk-)"
# Memo for cli_session_token: shelling out costs ~100ms and the token is stable
# for the session, so resolve it at most once per process. `False` means "asked
@@ -58,8 +65,8 @@ def cli_session_token() -> str | None:
def cloud_api_key() -> str | None:
"""Resolve the bearer credential.
Order: ``HANZO_API_KEY`` env, then the hk- key in ~/.hanzo/config.json,
then the CLI's live IAM session.
Order: ``HANZO_API_KEY`` env, then the key in ~/.hanzo/config.json, then
the CLI's live IAM session.
"""
env = os.environ.get("HANZO_API_KEY") or os.environ.get("HANZO_KEY")
if env and env.strip():
+1 -1
View File
@@ -65,7 +65,7 @@ For the typed Hanzo Cloud API client, install [`hanzoai`](https://pypi.org/proje
## Configuration
```bash
HANZO_API_KEY=hk-...
HANZO_API_KEY=sk-...
HANZO_BASE_URL=https://api.hanzo.ai
```
+1 -1
View File
@@ -10,7 +10,7 @@ import hanzoai.cloud
def test_client_constructs():
cfg = hanzoai.cloud.Configuration(host="https://api.hanzo.ai")
cfg.access_token = "hk-test"
cfg.access_token = "sk-test"
client = hanzoai.cloud.ApiClient(cfg)
assert client is not None
assert cfg.host == "https://api.hanzo.ai"