mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
a pk- key authenticates nobody, so stop telling readers it reads
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.
This commit is contained in:
@@ -134,6 +134,23 @@ short-term shape, but the real fix belongs upstream: when cloud's emission
|
||||
declares a bearer scheme, `access_token` starts working, and the header should
|
||||
move back into `Configuration` in one edit. Until then, one place sets it.
|
||||
|
||||
## A `pk-` is not a read key
|
||||
|
||||
`cloud.APIKeyPrefixes` is `{"pk-", "sk-"}`, and it is tempting to read that pair
|
||||
as a permission split — publishable for reads, secret for writes. It is not one.
|
||||
`middleware_identity.go` returns nil for **any** `pk-` before it ever consults the
|
||||
key store: "a key meant for a browser bundle must not read … resolvable, not
|
||||
authenticating". A `pk-` resolves to an org so an analytics beacon can be
|
||||
attributed to a tenant, and that is the whole of it.
|
||||
|
||||
So every route these examples use refuses a valid `pk-` exactly as it refuses no
|
||||
key at all — `/v1/tools` says why in as many words, `"a validated principal is
|
||||
required"` — and the 403 that comes back reads like a revoked key rather than the
|
||||
wrong shape of key. `examples/client.py` rejects a `pk-` up front for that reason,
|
||||
which is the same argument as the `access_token` note above: an unsendable
|
||||
credential and an unauthenticating one produce the same misleading refusal, so
|
||||
both are caught before the request goes out.
|
||||
|
||||
**Two spec defects were found and fixed upstream while regenerating at 3.1.5.** Neither was
|
||||
patched here; both are in `hanzoai/openapi` main:
|
||||
- `fc0c17a` — 35 `/v1/platform` operations carried no `responses`. OAS 3.x requires it and
|
||||
|
||||
@@ -50,8 +50,11 @@ with client as api:
|
||||
|
||||
Every route follows that shape: one `*Api` class per tag, one method per
|
||||
operation, typed models in and out. Keys come from
|
||||
[cloud.hanzo.ai](https://cloud.hanzo.ai) or `hanzo login`, in two shapes —
|
||||
`pk-` to read, `sk-` to write.
|
||||
[cloud.hanzo.ai](https://cloud.hanzo.ai) or `hanzo login` in two shapes, and only
|
||||
one of them works here. Use an `sk-`: it carries a principal, which every call
|
||||
above needs. A `pk-` is publishable — it is safe in a browser bundle precisely
|
||||
because it names an org and authenticates nobody, so cloud refuses it at the
|
||||
identity boundary and it reads nothing.
|
||||
|
||||
## Auth
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
File generated from our OpenAPI spec by Stainless.
|
||||
|
||||
This directory can be used to store example files demonstrating usage of this SDK.
|
||||
It is ignored by Stainless code generation and its content (other than this keep file) won't be touched.
|
||||
+11
-2
@@ -24,15 +24,24 @@ MODEL = os.environ.get("HANZO_MODEL", "zen4")
|
||||
|
||||
|
||||
def api_key() -> str:
|
||||
"""Fail loudly and early when the key is absent.
|
||||
"""Fail loudly and early when the key is absent or of the wrong shape.
|
||||
|
||||
Without this the SDK sends an unauthenticated request and the flow dies on a
|
||||
401 several frames deep, which reads like an API bug rather than an unset
|
||||
shell variable.
|
||||
|
||||
A ``pk-`` is rejected here for the same reason. It is the PUBLISHABLE shape:
|
||||
cloud resolves it to an org so a browser beacon can be attributed, but the
|
||||
identity boundary refuses it outright, so it never becomes a principal. Every
|
||||
route below wants one — ``/v1/tools`` says so in as many words, "a validated
|
||||
principal is required" — so a ``pk-`` collects the same 403 as no key at all,
|
||||
and that 403 reads like a revoked key rather than the wrong shape of key.
|
||||
"""
|
||||
key = os.environ.get("HANZO_API_KEY")
|
||||
if not key:
|
||||
raise SystemExit("HANZO_API_KEY is not set — export an IAM JWT or a pk-/sk- cloud key")
|
||||
raise SystemExit("HANZO_API_KEY is not set — export an sk- cloud key or an IAM JWT")
|
||||
if key.startswith("pk-"):
|
||||
raise SystemExit("HANZO_API_KEY is a pk- (publishable) key, which authenticates nobody — use an sk-")
|
||||
return key
|
||||
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
DELETE /v1/kv/{name} delete_kv_by_name tear down
|
||||
|
||||
This is the PROVISIONING plane. The per-key data plane the spec also describes
|
||||
(``/v1/kv/keys/{key}``) is not mounted anywhere — GET 404s and PUT/DELETE 405 at
|
||||
(``/v1/kv/keys/{key}``) is not mounted anywhere — every verb 404s at
|
||||
api.hanzo.ai, and kv.hanzo.ai 404s the whole prefix — so a set/get/delete
|
||||
round-trip on keys would be an example that cannot run. These three all answer
|
||||
403 unauthenticated, i.e. routed and gated.
|
||||
round-trip on keys would be an example that cannot run. These three answer 403
|
||||
"a validated principal is required" unauthenticated, i.e. routed and gated.
|
||||
|
||||
The delete runs in ``finally``, so a failed read still tears the store down
|
||||
rather than leaving it billable for the next run to collide with.
|
||||
|
||||
@@ -4,13 +4,12 @@
|
||||
MCP surface: each entry is a tool name, its description and its input schema.
|
||||
|
||||
A note on the MCP door, because it is easy to pick the wrong one. There is a
|
||||
live JSON-RPC endpoint at ``POST /v1/mcp`` that answers ``tools/list`` with the
|
||||
same catalog (730 tools at the time of writing) — but it is NOT in hanzo.yaml,
|
||||
so the generator emits no method for it and an example would have to bypass the
|
||||
SDK to call it, which defeats the point of an SDK example. Of the MCP routes
|
||||
that ARE declared, ``/v1/automations/mcp`` returns 405 at api.hanzo.ai. So this
|
||||
catalog read is the one that is both generated and served. When ``/v1/mcp`` is
|
||||
added to the spec, this flow should move to it.
|
||||
JSON-RPC endpoint at ``POST /v1/mcp`` that answers ``tools/list`` with the same
|
||||
catalog — but it is NOT in the document, so the generator emits no method for it
|
||||
and an example would have to bypass the SDK to call it, which defeats the point
|
||||
of an SDK example. Of the MCP routes that ARE declared, ``/v1/automations/mcp``
|
||||
404s at api.hanzo.ai. So this catalog read is the one that is both generated and
|
||||
served. When ``/v1/mcp`` is added to the spec, this flow should move to it.
|
||||
|
||||
python -m examples.tools
|
||||
"""
|
||||
|
||||
@@ -27,7 +27,7 @@ from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any, Dict, List, Final, Tuple, Union
|
||||
from functools import lru_cache # noqa: TID251 - not using Stainless SDK
|
||||
from functools import lru_cache # noqa: TID251 - the typed replacement is hanzoai-only
|
||||
|
||||
# Type aliases with full annotations
|
||||
ASTDict = Dict[str, Any]
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
if [[ -n "$1" && "$1" != '--'* ]]; then
|
||||
URL="$1"
|
||||
shift
|
||||
else
|
||||
URL="$(grep 'openapi_spec_url' .stats.yml | cut -d' ' -f2)"
|
||||
fi
|
||||
|
||||
# Check if the URL is empty
|
||||
if [ -z "$URL" ]; then
|
||||
echo "Error: No OpenAPI spec path/url provided or found in .stats.yml"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> Starting mock server with URL ${URL}"
|
||||
|
||||
# Run prism mock on the given spec
|
||||
if [ "$1" == "--daemon" ]; then
|
||||
npm exec --package=@stainless-api/[email protected] -- prism mock "$URL" &> .prism.log &
|
||||
|
||||
# Wait for server to come online
|
||||
echo -n "Waiting for server"
|
||||
while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do
|
||||
echo -n "."
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
if grep -q "✖ fatal" ".prism.log"; then
|
||||
cat .prism.log
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
else
|
||||
npm exec --package=@stainless-api/[email protected] -- prism mock "$URL"
|
||||
fi
|
||||
@@ -8,9 +8,9 @@ Covers two guarantees:
|
||||
`hanzo_zap.CloudClient.call(...)` — asserting the derived method, forwarded auth,
|
||||
and body — using a mock CloudClient, so no network and no `hanzo-zap` install.
|
||||
|
||||
This file used to lock the STAINLESS surface — `Hanzo`, `AsyncHanzo`, `Client`,
|
||||
`Stream`, `HanzoError`, … — and import `Hanzo` at module scope. Every one of those
|
||||
18 names was deleted by 648e7354 ("retire Stainless"), which replaced
|
||||
This file used to lock the hand-written surface that preceded generation —
|
||||
`Hanzo`, `AsyncHanzo`, `Client`, `Stream`, `HanzoError`, … — and import `Hanzo` at
|
||||
module scope. All 18 of those names went away at 648e7354, which replaced
|
||||
`pkg/hanzoai` with openapi-generator output whose entry point is `ApiClient` +
|
||||
`Configuration`. The import therefore raised at COLLECTION, so pytest reported an
|
||||
error and ran none of the transport tests below — which is how the whole suite
|
||||
|
||||
Reference in New Issue
Block a user