Framework
Document types you define: describe a record once, then create, list, submit and cancel documents against it.
Also for this capability: API · CLI · MCP · SDKs
Document types you define: describe a record once, then create, list, submit and cancel documents against it.
| Base URL | https://api.hanzo.ai |
| Operations | 19 |
| Auth | Authorization: Bearer $HANZO_API_KEY |
Specification
HIP-1126 · Framework — The DocType Engine — Draft · read the specification →
/v1/framework is document types an org defines: describe a record once — its
fields, naming rule, lifecycle, and which role may do what — then create, list,
submit and cancel documents against it. The engine is
github.com/hanzoai/framework, built on the metadata model
github.com/hanzoai/doctype; neither knows what HTTP is, and the cloud
subsystem at apps/framework is the adapter — the "place" in the engine's
layering (apps/framework/framework.go:6-14). This HIP states the store the
place opens, how a request becomes an engine caller, and the one surface that
cannot carry a schema.
Motivation
Several application lanes — cms, erp, help, knowledge, content, guide — each
need "a record type with permissions and a lifecycle". Building that per lane
is the same engine five times, drifting. The adapter re-exports the engine
vocabulary so every lane keeps one import and compiles unchanged
(apps/framework/framework.go:22-24), and the engine enforces permissions
itself, so there is no authorization logic here — a second copy would be a
second answer (apps/framework/framework.go:29-31).
Specification
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as in RFC 2119.
§1 The store
framework owns one store: the engine's single database, opened by cloud's
storage policy — sqlpool.Open("framework", DataDir), encrypted at rest under
a KMS-held master key (apps/framework/framework.go:73-85). Cloud names the
file rather than accepting a path from the library, because a name is what
keys an encrypted file and a handed-down path cannot be. The rows inside are
per-org — every DocType and document belongs to the caller's org — with
isolation enforced by the engine's own permission calculus.
§2 The addresses
Every route is under /v1/framework (manifest/apps.go:180). DocType
definition, roles, modules, the summary, document reads, submit and cancel are
typed operations; the DELETEs answer no body and carry no response schema. The
two document writes — POST /v1/framework/{doctype} and
PUT /v1/framework/{doctype}/{name} — are raw handlers and MUST stay raw until
the registry can carry them honestly: their request body is the document's own
field data, a flat object whose properties the DocType defines at run time, and
no Go struct both accepts that body verbatim and describes it. A reflected
schema would name the two path segments and nothing else — an SDK method that
cannot send a document. The three registry properties required to convert them
are enumerated at the registration, and a test holds the refusal until all
three exist (apps/framework/framework.go:130-147).
§3 Tenancy
The bridge parks the validated identity facts on the subtree before any route
runs (apps/framework/framework.go:107), and each operation turns the
validated principal's org into an engine Caller
(apps/framework/framework.go:249, HIP-0026); what the identity boundary
refuses, this surface refuses. On a fresh org the first caller to administer
DocTypes is seeded as its System Manager, after which only a System Manager or
a platform admin may define (apps/framework/framework.go:395-399) — the
engine's calculus, not the adapter's.
§4 Money, events, telemetry
framework is free, in those words (plugin/framework/main.go:21, cloud.Free;
not in spend.go:275). It publishes no events on the bus, and emits nothing to
observability beyond the request span every route gets.
§5 Stage
framework is beta — the manifest row declares Stage: Beta
(manifest/apps.go:180), so it is reached by flag while the raw document
writes in §2 wait on the registry properties that can type them. It is the
data core the application lanes stand on, so ga is where it is headed; the
promotion is the one manifest edit HIP-0139 §8.4 names.
§6 Upstream
framework derives from no third-party code. Its engine and metadata model are
the Hanzo modules hanzoai/framework v0.1.0 and hanzoai/doctype v0.1.0,
which this package adapts and re-exports.
Rationale
Value / engine / place, rather than one HTTP-aware engine, is what keeps the permission calculus testable without a server and reusable by lanes that are not HTTP at all. The cost is an adapter whose whole job is translation — principal to Caller, engine Code to HTTP status — and that cost is paid once here instead of once per lane.
Security Considerations
The wrong implementation answers with another org's records or lets a non-manager redefine a type out from under its documents. Both gates are the engine's: the adapter contributes only the validated org, never a client-supplied one, and deliberately holds no authorization logic that could disagree with the engine's answer. The store is encrypted at rest, so a copy of the deployment's data directory does not yield the documents without the key service.
Four surfaces
| Surface | Reaches this capability as | Coverage |
|---|---|---|
| REST | framework at its own prefix | 19 operations |
| CLI | hanzo framework … | 19 of 19 |
| SDK | FrameworkApi in every published client | 16 of 19 — the clients are generated at their own release |
| MCP | tool framework on https://api.hanzo.ai/v1/mcp | 17 operations, 3 under the document's own id — ask describe for the rest |
Quickstart
export HANZO_API_KEY=sk-... # console.hanzo.ai → API keysThen the first call — a read that needs nothing but the key. GET /v1/framework/roles, operation get_framework_roles:
hanzo framework roles listimport { Configuration, FrameworkApi } from 'hanzoai';
const api = new FrameworkApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.getFrameworkRoles();from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import FrameworkApi
client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = FrameworkApi(client).get_framework_roles()cfg := cloud.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := cloud.NewAPIClient(cfg)
resp, _, err := client.FrameworkAPI.GetFrameworkRoles(context.Background()).Execute()
if err != nil {
return err
}use hanzo_cloud::apis::{configuration::Configuration, framework_api};
let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();
let result = framework_api::get_framework_roles(&cfg, Default::default()).await?;import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.FrameworkApi;
ApiClient client = new ApiClient();
client.setRequestInterceptor(b -> b.header("Authorization", "Bearer " + System.getenv("HANZO_API_KEY")));
var result = new FrameworkApi(client).getFrameworkRoles();The method above is the one at the current release of the document. [email protected] (npm) was generated from an earlier release, where this operation carried a different id, so it spells the method differently — regenerating the clients is what makes the two agree. SDKs →
curl https://api.hanzo.ai/v1/framework/roles \
-H "Authorization: Bearer $HANZO_API_KEY"MCP reaches framework through the framework tool, which names its 17 operations with its own verbs — this one among them, under a name only MCP declares. describe explains any of them:
curl -X POST https://api.hanzo.ai/v1/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "describe",
"arguments": {
"op": "get_framework_by_doctype"
}
}
}'Answers 200 with object — ok.
Endpoints
| Endpoint | What it does |
|---|---|
POST /v1/framework/{doctype}/{name}/cancel | Moves a submitted document to cancelled (docstatus 1 → 2) after its on_cancel hooks agree. |
POST /v1/framework/{doctype}/{name}/submit | Moves a draft to submitted (docstatus 0 → 1) after its on_submit hooks agree. |
GET /v1/framework/{doctype}/{name} | Returns one document by name, with Password fields redacted. |
PUT /v1/framework/{doctype}/{name} | Replace a draft document's field data wholesale. |
DELETE /v1/framework/{doctype}/{name} | Removes one document, after its on_trash hooks agree. |
GET /v1/framework/{doctype} | Returns the caller org's documents of one DocType, filtered, ordered and projected by the query. |
POST /v1/framework/{doctype} | Create one document of a DocType, from that DocType's own fields. |
GET /v1/framework/doctypes/{name} | Returns one DocType definition — its fields, naming rule, permissions and lifecycle flags. |
PUT /v1/framework/doctypes/{name} | Replaces a DocType definition wholesale (PUT semantics): the stored definition becomes the body. |
DELETE /v1/framework/doctypes/{name} | Removes a DocType and every document stored under it. |
GET /v1/framework/doctypes | Returns every DocType defined in the caller's org. |
POST /v1/framework/doctypes | Defines a DocType in the caller's org: the metadata that gives a document surface its fields, its naming rule, whether it has a submit/cancel… |
POST /v1/framework/modules/{module}/install | Creates an app lane's DocTypes in the caller's org. |
GET /v1/framework/modules/{module} | Returns one app lane's install state for the caller's org: the DocTypes the lane declares, and which of them already exist in the org. |
GET /v1/framework/modules | Returns every app lane compiled into this deployment and the DocTypes each one installs. |
DELETE /v1/framework/roles/{user}/{role} | Removes one (user, role) grant in the caller's org. |
GET /v1/framework/roles | Returns every (user, role) assignment in the caller's org. |
POST /v1/framework/roles | Grants one user one role in the caller's org — how a member gains rights on a DocType, since permissions name roles and never users. |
GET /v1/framework/summary | Reports how much of the DocType surface the caller's org uses: how many DocTypes it has defined, and how many documents exist across them. |
How is this guide?