Knowledge
Your team's wiki and your agents' memory, searchable by meaning.
Also for this capability: API · CLI · MCP · SDKs
Your team's wiki and your agents' memory, searchable by meaning.
| Base URL | https://api.hanzo.ai |
| Operations | 19 |
| Auth | Authorization: Bearer $HANZO_API_KEY |
Specification
HIP-1260 · Knowledge — Wiki and Agent Memory — Draft · read the specification →
/v1/knowledge is one organization's knowledge, searchable by meaning: wiki
pages a person writes, memories an agent files, and documents a connector
ingests are ONE document store, indexed into that organization's own vector
namespace on every save and read back as semantic search, a link graph, or an
imported vault. It is implemented in hanzoai/cloud at apps/knowledge. This
HIP states the property everything rests on — human wiki and agent memory are one
store indexed once, so an agent retrieves exactly what the team can read — and
the surface that exposes it.
Motivation
A team wiki and an agent's memory are usually two systems: two stores, two
permission models, two search indexes, and an integration that copies one into
the other and drifts. Here a wiki page IS a document, a memory IS a
document, an ingested Slack thread IS a document (apps/knowledge/kb.go:15-24),
and one indexing path serves them all — so "what does this org know about X"
has one answer whether a person or an agent asks.
Specification
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as in RFC 2119.
It owns no store
The documents are the framework engine's, as module kb
(apps/knowledge/kb.go:40): CRUD, permissions and tenant isolation are the
framework's generic surface, and this capability adds behaviour, not storage.
The vectors are the shared vector store's, one collection per organization,
written only through the one index path (apps/knowledge/index.go:22-27).
Both stores are owned elsewhere; this capability MUST NOT open a store of its
own, and any new document kind MUST enter as a framework DocType so the same
hooks index it.
The address
The capability answers under /v1/knowledge: semantic search (the RAG entry
point), the link graph, vault import, and the connector set — list, catalog,
connect, callback, sync, disconnect (apps/knowledge/subsystem.go:84-99).
Today's router still serves this surface at /v1/kb; that pair is carried by
hanzoai/cloud openapi/misfiled.txt and closes by fold, the route group
being one literal (apps/knowledge/subsystem.go:82).
Every operation is typed except the import, which cannot be: its body is the
upload itself — a vault zip, an .enex XML document or a JSON export chosen by
?format= — not JSON a typed input could decode, so it is declared with prose
beside the route (apps/knowledge/subsystem.go:92-99,
apps/knowledge/import.go:129-150). An importer normalizes to one pure shape
(apps/knowledge/vault/vault.go) and files through the same ingest path a
connector uses; no importer MAY write the store or the index directly.
Tenant
Every handler resolves its organization from the validated principal —
principal.Acting (apps/knowledge/subsystem.go:141,
apps/knowledge/connectors.go:223) — and a request without one is refused.
Isolation at the index is physical AND filtered: each organization has its own
collection and every point carries the organization in its payload, so a
search must pass both (apps/knowledge/index.go:31-35, :297).
Meter, events, observability, stage
Metered, and the unit is one connector piece run (plugin/knowledge/main.go,
Price: cloud.Metered): a long-tail connector's sync executes a JavaScript
piece on the auto engine's sandbox pods, and reaching that capacity by
in-cluster URL does not make the pod cheaper. The fee is
CLOUD_KB_FEE_CENTS_PIECE over CLOUD_KB_FEE_CENTS, defaulting to one cent —
sized like the compute it buys, one bounded execution on a pod already held —
gated before the engine is asked and debited only after it answers
(apps/knowledge/meter.go). The native-Go connectors start no pod and stay
free, as does everything else on the surface. The other debit lands through
ai: embeddings for index and query go the metered gateway path with the same
model on both sides (apps/knowledge/index.go:50-51).
It publishes no events on the bus. Beyond the request span it registers
nothing; its degradations are on the wire instead — indexing is fail-open (a
save never blocks on the index) and query is fail-honest, answering an empty
result with degraded: true rather than a 5xx or a fabricated hit
(apps/knowledge/index.go:37-42, apps/knowledge/subsystem.go:124-127).
Its stage is ga.
Upstreams
It derives from none: no OSS project is forked, embedded or mirrored. The
importers implement third-party export formats from their public shapes, in
pure Go with no upstream code taken — the Lexical EditorState JSON the editor
renders (apps/knowledge/lexical/lexical.go), Obsidian vault markdown, Roam's
JSON export, Evernote ENEX/ENML, and the Notion API's result shapes
(apps/knowledge/{obsidian,roam,evernote,notion}).
Rationale
The alternative is a dedicated knowledge service with its own database and its own permission model. It buys nothing the framework does not already have and costs a second tenancy implementation — the exact place a wiki leaks. Attaching behaviour to the framework's store means the fourth app lane after cms, erp and help reuses isolation that is already tested, and the only new code is the one index path and the pure normalizers.
Fail-open indexing was chosen because the store is the record and the index is derived: losing a search hit until reindex is recoverable; losing a save is not.
Security Considerations
The surface is an organization's institutional memory — the most valuable
single corpus a tenant hands us — so the wrong implementation leaks a whole
company at once. The cross-tenant argument is doubled on purpose: a
collection-name bug cannot leak because the payload filter still excludes
foreign points, and vice versa (apps/knowledge/index.go:31-35).
Connector tokens are third-party credentials (Slack, GitHub, Google). They
live in KMS at a per-org path; the connector document holds only the path and
non-secret metadata, and the token is never logged
(apps/knowledge/connectors.go:13-15). The OAuth state is HMAC-bound to the
organization (apps/knowledge/connectors.go:16), so a callback cannot be
replayed into another tenant's connection.
The import route parses attacker-supplied archives; every read is bounded
(apps/knowledge/import.go:142, :174), so a crafted zip exhausts a limit, not
the process. And because everything indexed is retrieved into agent context,
ingested text is data, never instructions — the retrieval surface returns
documents and MUST NOT execute them.
Four surfaces
| Surface | Reaches this capability as | Coverage |
|---|---|---|
| REST | knowledge at its own prefix | 19 operations |
| CLI | hanzo knowledge … | 10 of 19 — the CLI pins the document on its own clock |
| SDK | KnowledgeApi in every published client | 19 methods |
| MCP | tool knowledge on https://api.hanzo.ai/v1/mcp | 9 operations, 1 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/knowledge/files, operation get_knowledge_files:
hanzo has no subcommand for this operation — the CLI serves only what cloud's live route table confirms. Use HTTP or an SDK.
import { Configuration, KnowledgeApi } from 'hanzoai';
const api = new KnowledgeApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.getKnowledgeFiles();from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import KnowledgeApi
client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = KnowledgeApi(client).get_knowledge_files()cfg := hanzoai.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := hanzoai.NewAPIClient(cfg)
resp, _, err := client.KnowledgeAPI.GetKnowledgeFiles(context.Background()).Execute()
if err != nil {
return err
}use hanzo_client::apis::{configuration::Configuration, knowledge_api};
let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();
let result = knowledge_api::get_knowledge_files(&cfg, Default::default()).await?;import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.KnowledgeApi;
ApiClient client = new ApiClient();
client.setBearerToken(System.getenv("HANZO_API_KEY"));
var result = new KnowledgeApi(client).getKnowledgeFiles();curl https://api.hanzo.ai/v1/knowledge/files \
-H "Authorization: Bearer $HANZO_API_KEY"MCP reaches knowledge through the knowledge tool, which names its 9 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": "list_knowledge_connectors"
}
}
}'Answers 200 with object — ok.
Endpoints
| Endpoint | What it does |
|---|---|
GET /v1/knowledge/connectors/{provider}/callback | Finishes an OAuth connection: it exchanges the provider's code for a token, seals that token in KMS, and records the connection. |
GET /v1/knowledge/connectors/{provider}/connect | Returns the provider authorize URL the console opens to connect this org's account. |
POST /v1/knowledge/connectors/{provider}/sync | Pulls the provider's documents for the caller's org and files them as knowledge sources, which the store's own hook then indexes — so a synced document is retrievable exactly like a hand-written page. |
DELETE /v1/knowledge/connectors/{provider} | Revokes a connection: it tombstones the stored credential so a later sync cannot reuse it, removes this provider's passages from the org's store, and marks the connector disconnected. |
GET /v1/knowledge/connectors/catalog | Returns the ONE catalog of everything a caller can connect: every first-party connector and every long-tail one, in a single list sorted by provider. |
GET /v1/knowledge/connectors | Returns every supported knowledge connector with THIS org's connection state and the REAL number of documents each has ingested into the org's store. |
GET /v1/knowledge/files/{id}/graph | Answers one of the caller's org files' place in the org's graph: the files it links to and is linked from (a hyperlink, a citation by name), the files that name the same entities, and the entities it names. |
GET /v1/knowledge/files/{id}/sections/{section} | Answers one section of one of the caller's org files: its own text, its subsections, the sections it links to or shares entities with — in this file or another — and the entities it names. |
GET /v1/knowledge/files/{id}/toc | Answers one of the caller's org files' table of contents: every section in document order with its depth, its parent and a one-line summary. |
GET /v1/knowledge/files/{id} | Answers one of the caller's org files: what it is, where its bytes are, and how far its ingest has got. |
DELETE /v1/knowledge/files/{id} | Removes one of the caller's org files from the index — its table of contents, passages, full-text rows, mentions and links — and its record. |
POST /v1/knowledge/files/retrieve | Grounds an answer in the caller's org files, table of contents first: candidate documents are found by searching their passages; a model reads their tables of contents — titles and one-line summaries — and picks the sections the answer is in; hybrid search drills into the passages of those sections; and the graph expands to the sections they link to or name the same entities as, in any file of the workspace. |
POST /v1/knowledge/files/search | Answers the passages of the caller's org files that match a query, each citing its file › section › paragraph: the search behind Drive's box, and the first step an agent takes across a workspace. |
GET /v1/knowledge/files | Answers the caller's org files, most recently changed first, each with its status and stage — optionally only those in one bucket, which is how Drive shows a folder's files with their index state. |
POST /v1/knowledge/files | Makes an object in one of the caller's org buckets a workspace file. |
GET /v1/knowledge/graph | Returns the caller org's knowledge as a node/edge graph shaped for a force-directed renderer: pages, memories and synced sources as nodes; the page parent tree, the wikilinks between pages, and each source's connector provenance as edges. |
POST /v1/knowledge/import | Import an Obsidian, Notion, Roam or Evernote export into the org's knowledge base |
POST /v1/knowledge/reindex | Rebuilds the caller org's retrieval from its documents: every passage is removed, and every page, memory and source is cut into passages and embedded again with the configured model; the lexical index is reconciled to the same documents. |
POST /v1/knowledge/search | Runs a hybrid search over the caller org's own knowledge — its wiki pages, its agent memories and everything its connectors have synced. |