P1 — the primary Create now produces something, and Chat reaches the gateway:
- middleware/gateway_auth.py: ONE fail-closed allowlist for the browser-edge
gateway bearer — a server key first, else pk-/asymmetric-JWT only; the HS256
hanzo.id session token and sk-/hk-/rk- secrets are NEVER forwarded (the old
"unsupported signing method: HS256" 502). copilot._resolve_target adopts it.
- Z-Image-Turbo text-to-image lane: _image_graph + dispatch_image + POST
/v1/library/image. The composer's default up-arrow (words, no photo) now
generates an image instead of the old "attach a photo first" dead end.
- composer defaults to an IMAGE model (hz_create_model, decoupled from the chat
model); routes by modality (assistant -> Chat via window.HanzoChat, else
image/video/fix/compose); create() ALWAYS shows a generating state + friendly
retry, never a silent no-op.
- shell.js: copilot errors render one friendly line (never raw JSON); chat empty
state (assistant identity + 3 starters); one public HanzoChat.send hook.
P2 — friction on the common path:
- first-class Download on every tile AND in the viewer (PNG / JPG / WebP)
- opaque fixed header + sticky live-bar (content no longer scrolls under them)
- ARCHIVED badge only on truly deleted/flagged items (active work is unbadged)
- mobile: ~30 filter pills collapse to one scrollable row; tab-bar scroll fade
P3 — composer pill tooltips, per-model descriptions, code model hidden, and the
</> request-preview gated behind Developers mode.
Tests: gateway_auth_test (allowlist refuses HS256/sk-, accepts pk-/asymmetric),
copilot_test (never forwards the HS256 session token), create_image_test
(Z-Image graph shape + served-HTML invariants: default-model-is-image,
Create-always-shows-a-state, first-class Download). Green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a first-class office-document capability ALONGSIDE the ComfyUI/GPU creative
pipeline: structured content (typed template fields, filled by a person or the
AI) → a pure-Python renderer → a real .pdf/.docx/.xlsx/.pptx/.md. No graph, no
GPU. Orthogonal subsystem — not bolted onto graphs.
- middleware/doc_render.py: one neutral IR per archetype (doc/sheet/deck) and
exactly one renderer per format (DRY). PDF via WeasyPrint (full-CSS monochrome
print style) with a self-contained pure-Python PDF fallback (valid %PDF, zero
native deps — so CI/model-less pods still render). DOCX/XLSX/PPTX via
python-docx/openpyxl/python-pptx.
- middleware/documents.py: one declarative CATALOG of 22 common templates
(Documents 15, Spreadsheets 4, Presentations 3). Each template's typed fields
drive the form, the AI-fill JSON, and the renderer inputs (one schema). Per-org
store (orgs/<org>/documents, atomic index.json + <id>/doc.json), rendered on
demand. Endpoints (/v1 house style): GET /v1/documents, POST
/v1/documents/generate, GET /v1/documents/library, GET|PATCH|DELETE
/v1/documents/{id}, GET /v1/documents/{id}/download?format=. Registered via the
same injected tenancy resolvers as stacks_store.
- AI-fill is a layered enhancement — the form path renders with NO LLM, so a
broken/unauthorized gateway never blocks creation (422 needFields → prefilled
form). Reuses the shipped Chat's working token: the browser sends window.HZ.pk
(publishable pk- key) as the bearer; the backend forwards it fail-closed —
server key, else a gateway-verifiable pk-/RS-JWT; an HS256 session token or
sk-/hk- secret is NEVER forwarded. Model default enso (window.HZ.model).
- studio_home.html: a "Documents" Home tab (searchable catalog + your documents
with Download PDF/Word/Excel/PowerPoint) + describe/fill dialog; replaced the
dead doc/sheet/paper composer placeholders with one "Office documents" card.
- deps: weasyprint/python-docx/openpyxl/python-pptx (requirements.txt) + pango
native libs (Dockerfile). CI builds the image; not built locally.
- tests-unit/documents_test: catalog invariants, full template×format render
matrix asserted by magic numbers, AI-fill parse + fail-closed bearer allowlist,
and the REST surface incl. per-org isolation (44 tests, green).
- version 0.17.24 → 0.18.0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make EVERY studio render flow through the org's visible gpu-jobs queue and
eliminate the direct-to-127.0.0.1:8188 bypass. Studio-side only (cloud CLI /
worker two-lane claim owned by the cloud agent).
- gpu_dispatch: per-GPU targeting. X-Target-GPU -> taskQueue "gpu:<identity>"
(namespace stays gpu-jobs); an explicit target always enqueues. Untargeted =
shared "gpu-jobs" lane.
- server.post_prompt: in --worker-mode refuse a direct /prompt POST without
X-Worker-Token (403) via worker_client.reject_untrusted_worker_submit (one
policy gate). The only accepted execution is a claimed job over
/v1/worker/execute. Legacy prompt_router push is OFF unless
STUDIO_LEGACY_PUSH_ROUTER=1 -> gpu-jobs enqueue is the ONE submit path.
- worker_client: /v1/worker/execute validates X-Worker-Token, runs the graph on
the local prompt_queue, scopes outputs to the job's org, returns {prompt_id}.
- studio_home: /v1/render-queue + /v1/nodes are AUTHORITATIVE — read the cloud
tasks engine's gpu-jobs activities (real claiming identity, status ->
queued/running/done/failed, lastHeartbeatTime, per-node depth). render_jobs.json
is only a ~2s pre-claim placeholder. X-Target-GPU forwarded through _queue_prompt.
- UI: "Run on <gpu>" chips (studio_home.html/shell.js) set X-Target-GPU; in-flight
rows show the REAL worker + queued/running state.
Tests: gpu_dispatch_test (targeting), worker_seam_test (gate 403 + claim seam).
378 passed. Follow-up (cloud agent): content/studio_render.go:86 convergence.
Co-authored-by: hanzo-dev <dev@hanzo.ai>
Full Stacks spec on the /studio home surface: create by drag (image→image) or
multi-select; layered Stack cards (cover/name/count/updated); a detail view with
add/remove/reorder/move/set-cover/rename/describe/unstack/delete; the delete
dialog defaulting to preserve; generation 'Save outputs to' routing (fix/compose/
rerun/template) with per-org auto-stack-batch setting; keyboard + touch parity
(no drag-and-drop ever required); a11y labels + live region; loading/empty/missing
states.
Backend: middleware/stacks_store.py — per-org SQLite (WAL, BEGIN IMMEDIATE) with
the spec's stacks + stack_assets join, UNIQUE(asset_id) = one Stack per asset,
tenancy structural (one file per org). Replaces the JSON whole-set /v1/stacks
store with the full REST surface. Ingest assigns membership; remove != delete;
delete-images maps to the library soft-delete. Tests: tests-unit/studio_home_test/
test_stacks.py.
Co-authored-by: Hanzo AI <ai@hanzo.ai>
Top-right user menu driven by GET /v1/session (user, org switcher,
project scope, Console link, Logout). Org switching is real via one
hook: the auth middleware applies session.resolve_org to a COPY of the
validated user so iam_user["org_id"] -- the id every tenancy/output
path already reads -- reflects the selection; membership from
_orgs_from_claims (owner + organizations/orgs/groups). POST
/v1/session/{org,project} set validated cookies; /logout clears the
session + selection cookies and bounces to the IAM end-session -> /login.
Frontend is a SPA-agnostic pill + dropdown (branding/hanzo-identity.*),
injected like the copilot (patch_identity.py, apply-branding step 10).
patch_destring.py (step 11) sweeps English display 'ComfyUI' -> 'Hanzo
Studio' (boundary-safe: skips code identifiers, i18n keys, hyphenated
product names, and non-English locale bundles) and rebrands the Settings
section header value 'Comfy' -> 'Studio' / 'Comfy-Desktop' -> 'Studio
Desktop' while preserving the setting KEYS. Served English display
'ComfyUI' 63->0. Tests: middleware_test/session_test.py (12 new).
Backend POST /v1/copilot/chat (middleware/copilot.py, registered in server.py)
runs one OpenAI tool-calling round to a chat endpoint (STUDIO_COPILOT_URL,
default local engine :1234 else api.hanzo.ai) and returns {reply, ops}. add_node
ops validated against nodes.NODE_CLASS_MAPPINGS so the model can't invent nodes.
Frontend bundle (branding/hanzo-copilot.js + on-brand css) registers a Copilot
sidebar tab via app.extensionManager.registerSidebarTab and applies ops against
app.graph (LiteGraph): add/set_widget/connect/set_prompt/move/delete/layout/queue.
Every op guarded. Injected by branding/patch_copilot.py (step 9 of apply-branding.sh),
survives frontend upgrades. window.HanzoCopilot.applyOps exposed for tests.
Tests: middleware_test/copilot_test.py (13, op-validation + mocked tool loop),
branding/copilot_smoke.py (headless: applier mutates a mock graph + panel renders).
Live route needs one server restart to register.
SqlitePromptQueue (middleware/tasks_queue.py): drop-in for PromptQueue backed
by one SQLite file (stdlib sqlite3, WAL, zero external processes). Opt in with
STUDIO_QUEUE_DB; precedence STUDIO_QUEUE_DB > STUDIO_PERSIST_QUEUE > memory so
local default is unchanged. The put/get/task_done seam maps to a durable job
state machine: idempotent submit (INSERT OR IGNORE on prompt_id), exactly-once
claim (BEGIN IMMEDIATE), retry-on-error, and crash-recovery via a lease + reap
that re-runs an abandoned claim (idempotent — SaveImage suffixes increment).
Multiple Studio processes on the same DB compete safely.
Engine selector (middleware/engine_selector.py): GET /v1/engines lists execution
targets for the org (local + registered compute_config workers), PUT
/v1/engines/default sets a per-org default stored on ComputeProfile; route_prompt
honors it (local = unchanged). Leased cloud machines are a future engine class
(interface stubbed). Frontend picker is a TODO.
docs/federation.md rewritten to the durable-queue reality with one future
paragraph (remote Tasks backend + Go/unified-binary HIP-0106 migration + leased
machines). Tests: middleware_test/{tasks_queue,engine_selector}_test.py, incl.
crash-recovery across queue instances.
Auth (middleware/iam_auth_middleware.py): local JWKS JWT validation against
Hanzo IAM (signature + exp + iss + aud=hanzo-studio; org from the `owner`
claim). Standard OIDC Authorization Code flow with PKCE and a /callback
session cookie for the standalone studio.hanzo.ai app; anonymous localhost
bypass unchanged. verify_jwt() extracted as a pure, unit-tested function.
Tenancy (folder_paths.py, app/user_manager.py): make get_public_user_directory
org-rooted so multi-tenant userdata resolves under user/orgs/{org}/... (it
previously always returned None). Scope execution outputs to output/orgs/{org}/
via set_execution_org(), bound by the prompt worker from the queue item org_id.
Also block a token subject from escaping into a System User namespace.
Federation (middleware/{worker_client,prompt_router}.py, server.py): move the
worker registry + execute surface to /v1/workers/register, /v1/workers,
/v1/worker/execute; add X-Worker-Token coordinator trust. Outbound pull channel
for NAT'd local boxes (GB10) specced in docs/federation.md.
Deploy: hanzo.yml + .github/workflows/cicd.yml (hanzoai/ci reusable) build
ghcr.io/hanzoai/studio and roll the studio operator Service CR at
studio.hanzo.ai. PyJWT[crypto] pinned.
Tests: tests-unit/middleware_test/iam_auth_test.py (RS256 sign/verify, state
CSRF, path exemptions) + folder_paths_test/org_scoping_test.py; 140 passing
across the auth/tenancy/user-manager/prompt-server suites.