Member API
The authenticated member surface, mounted at /api.
Authentication
Every request carries a validated Kamiwaza identity envelope. Kaizen does not issue tokens, maintain a session, or accept identity from a request body.
| Field | Header | Role |
|---|---|---|
| user id | x-user-id | Stable platform id |
x-user-email | The document-ACL principal, lowercased | |
| display name | x-user-name | Display only |
| roles | x-user-roles | Comma-separated, normalized to lowercase |
A missing envelope returns unauthenticated. An authenticated identity with no
email returns forbidden — Kaizen's ACL is email-based and cannot map safely
without one. Both emit an access-denied audit event with a bounded reason.
GET /api/auth/me also projects the optional
session_idle_timeout_minutes browser policy. Null means the SPA runs no Kaizen
inactivity timer and follows Kamiwaza expiry. This is policy, not
identity authority or backend deadline enforcement.
It also projects one *_enabled boolean per optional surface, so a client can
avoid offering something this deployment does not mount. These are advisory:
every optional route is flag-mounted and answers 404 when its surface is off,
and each one re-checks its own gate. A capability is named for the setting it
reports — sandbox_browser_sessions_enabled for
SANDBOX_BROWSER_SESSIONS_ENABLED — because the release gate pairs the two by
name when deciding whether a disabled surface excuses its acceptance journey.
GET /api/auth/me also includes agent_pipeline_options. The field is an array
of source-reviewed {id, version, label, description} records. It remains empty
unless AGENT_PIPELINE_SELECTOR_ENABLED is on, the caller has live Kaizen Admin
authority, and the caller's opaque client-preference scope is on the static
selector allowlist. The browser treats an empty array as one disabled Tomo
option in the pipeline pane of model and run settings. It never submits a
pipeline selection.
The renderer-pack host adds artifact_renderer_packs and
artifact_renderer_receipt_sha256 to /api/auth/me. The status list includes
installed pack identities, enabled state, and exact renderer/payload/version
claims; disabled installation is diagnostic, not execution authority. The
receipt digest identifies the exact image-installed receipt bytes, which the
browser must match against the web image before selecting a pack.
Artifact detail responses, including conversation-scoped detail, add optional
artifact_renderer_admission. A non-null admission names the exact pack and
claim and binds approval to the content_sha256 of that response's stored
content. Clients must verify that digest and current pack/receipt agreement
before mounting a renderer. Absent, disabled, unknown, or rejected packs leave
admission null and preserve readable text/JSON fallback. See
Artifact renderer packs for the complete
mount and deployment contract.
GET /api/auth/login accepts a return_to URL only when it matches Kaizen's
configured public origin and remains under the configured app base path.
When the configured root is portless, the browser authority preserved by the
immediate web proxy may supply only the external port for that same hostname;
the accepted hostname, scheme, and base path remain constrained by
configuration. That proxy addresses its own hop with the upstream service
authority so a service mesh can route it, and forwards the browser authority as
X-Forwarded-Host, falling back to Host when it is absent. An explicit
configured port cannot be replaced, malformed authorities fall back to the
configured root, and a forwarded authority naming any other hostname is
discarded rather than honoured. The route then redirects to Kamiwaza sign-in with
that accepted target in the platform's redirect_uri parameter. POST /api/auth/logout
coordinates server-side and
front-channel Kamiwaza logout and applies the same validation to
post_logout_redirect_uri.
Conventions
- Errors are bounded reason codes, not messages.
not_founddeliberately covers "never existed", "you lost access", and "was deleted". - Mutations accept an idempotency key. Reusing a key with different content is rejected rather than creating ambiguous duplicate work.
- Optimistic concurrency uses revisions; a conflicting edit returns a version conflict rather than silently overwriting.
- Streaming endpoints use SSE and resume from a durable position. See Streaming & event contracts.
Routes by domain
Session
GET /api/auth/login
GET /api/auth/me
POST /api/auth/logout
GET /api/models
GET /api/suggestions
GET /api/brand/brand.css | config.js | logo | favicon
Authenticated model rows distinguish the selectable deployment id from
runtime_model_id, the canonical identifier retained in durable run attribution.
Clients must not assume those identifiers are equal.
Search and documents
POST /api/search
GET /api/documents/{chunk_id}
Chat
POST /api/chat/prewarm
POST /api/chat/feedback
The prewarm route is speculative cache work only. It cannot accept, execute, or persist a logical turn. There is no separate Chat execution endpoint.
Conversations
GET /api/assistant-profiles
POST /api/conversations
GET /api/conversations
GET /api/conversations/{id}
PATCH /api/conversations/{id}
DELETE /api/conversations/{id}
POST /api/conversations/{id}/condense
GET /api/conversations/{id}/condense/{task_id}/events
GET /api/conversations/{id}/events
POST /api/conversations/{id}/inputs
POST /api/conversations/{id}/inputs/{input_id}/resume
POST /api/conversations/{id}/typing
POST /api/conversations/{id}/artifact-events
DELETE /api/conversations/{id}/presence
POST /api/conversations requires an Idempotency-Key and creates an empty
private room before its first input. Every private or shared message, steer,
stop, and cancel then enters the same inputs endpoint and append-only journal.
There is no private-chat bypass. GET /api/conversations/{id} derives viewer
capabilities from current server authority; viewer.active_input_id identifies
the viewer's cancellable accepted input in either room mode. Hydration may wake
that exact input through resume, which rechecks current write access and actor
identity before scheduling recovery. An input that has already settled is not an
error to wake: resume answers 200 with accepted: false and the input's
terminal status, so a client that hydrated moments before the run finished learns
the outcome instead of a 404.
Allowlisted administrators create and inspect explicit agent pipeline assignments through two protected routes:
POST /api/agent-pipelines/conversations
GET /api/agent-pipelines/conversations/{id}
The POST requires Idempotency-Key and exactly
{pipeline_id, pipeline_version}. It returns the conversation id and a locked
assignment containing the exact pipeline id, version, display label, assignment
source, and policy version. The GET returns that locked assignment only when
the caller owns the conversation. Shared-chat collaborators receive the same
HTTP 404 as an absent route, even if they can read the conversation. Unknown,
disabled, or incompatible selections fail admission. A caller who fails either
eligibility check also receives the same HTTP 404.
GET /api/assistant-profiles requires the platform identity envelope and
returns a versioned list of personal, data, kamiwaza, and general
profiles. A native client may send an optional
{"assistant_profile":"personal"} body to POST /api/conversations with its
usual Idempotency-Key. The server stores the choice on the canonical room;
creation without a body remains valid and unprofiled. Creation retry with a
different profile returns a conflict, including when one retry omits the body.
List and detail responses carry the stored assistant_profile or null. Profile
identity is not accepted on an input request and never supplies an actor or a
credential. General is pinned to the baseline Kaizen pipeline, and the server
denies private resource, tool, delegation, and task selectors for that room.
Data may use online public search only when that tool is separately configured
and authorized for the run; the profile itself is not a tool grant.
artifact-events accepts one bounded click, input, change, keydown, or keyup
for an exact active artifact linked to the room. The server derives the actor
from the platform envelope, requires current room write access, rechecks the
active resource link, and relays the event as transient room state. It keeps a
bounded recent reconnect window; this endpoint is not a durable application
state API.
The Chat /compact command calls the owner-scoped condense pair above. The
POST accepts an Idempotency-Key and returns a task pointer; the GET streams
its exclusive lifecycle. The command itself never enters the chat journal or
the agent-turn endpoint. Acceptance binds the task to the current canonical
journal head. Completion appends an immutable context-projection checkpoint;
future turns use its summary plus later journal events, so private and shared
rooms do not depend on actor-local Pi session history.
Collaboration
POST /api/conversations/{id}/invitations/preflight
POST /api/conversations/{id}/invitations
POST /api/conversations/{id}/invitations/{invitation_id}/resend
DELETE /api/conversations/{id}/invitations/{invitation_id}
GET /api/conversation-invitations
POST /api/conversation-invitations/{invitation_id}/accept
DELETE /api/conversation-invitations/{invitation_id}
GET /api/conversations/{id}/participants
DELETE /api/conversations/{id}/participants/{membership_id}
PUT /api/conversations/{id}/participants/{membership_id}/access
POST /api/conversations/{id}/leave
PUT /api/conversations/{id}/agent
Conversation and Project invitation acceptance derives the actor only from the forwarded platform envelope. A signed-in named recipient may accept from the inbox without a token; the optional recipient-bound token lets the SPA submit a deep-link acceptance even when its cached inbox has not refreshed. The same recipient, workroom, lifecycle, expiry, and participant-limit checks run in both paths, and errors disclose no conversation or Project metadata.
Resources linked to a conversation:
POST /api/conversations/{id}/resources
POST /api/conversations/{id}/resources/{ref}/versions
DELETE /api/conversations/{id}/resources/{ref}
GET /api/conversations/{id}/resources/{ref}/preview
GET /api/conversations/{id}/resources/{ref}/preview-page
GET /api/conversations/{id}/resources/{ref}/content
GET /api/conversations/{id}/artifacts/{artifact_id}
A version link names the reference the client rendered. If that reference has
since been retired but the room already links the exact version being named
(a completed generation links itself), the request answers with the existing
link. A version that is still being written is accepted with the room's current
link for that root, because completion links the finished version itself. Only
a version nothing active links, and nothing is writing, answers 404.
Projects
GET /api/projects
POST /api/projects
GET /api/projects/{id}
PATCH /api/projects/{id}
DELETE /api/projects/{id}
GET /api/projects/{id}/deletion-impact
GET /api/projects/{id}/eligible-chats
POST /api/projects/{id}/chats
PUT /api/projects/{id}/chats/{conversation_id}
DELETE /api/projects/{id}/chats/{conversation_id}
PUT /api/projects/{id}/library/{item_kind}/{item_id}
DELETE /api/projects/{id}/library/{item_kind}/{item_id}
PATCH /api/projects/{id}/library/{item_kind}/{item_id}/access
GET /api/projects/{id}/library-preference
PATCH /api/projects/{id}/library-preference
GET /api/conversations/{id}/project-impact
Project membership mirrors conversation membership, including
/api/project-invitations and per-member removal-impact endpoints.
Document access updates are contributor-owned and writer-gated. The preference
is per member and per Project; a missing row means new document links are
shared. Other Library kinds keep their source-specific authorization.
The impact endpoints are worth noting: destructive operations expose a preview so a client can show consequences before committing.
Library
GET|POST /api/collections
GET|PATCH|DELETE /api/collections/{id}
POST|DELETE /api/collections/{id}/documents[/{chunk_id}]
GET|POST /api/prompts
GET|PATCH|DELETE /api/prompts/{id}
POST /api/prompts/{id}/use
POST /api/prompts/{id}/validate
POST /api/prompts/{id}/run[/stream]
POST /api/prompts/{id}/debug[/stream]
GET /api/prompts/{id}/runs
GET /api/prompts/{id}/runs/{run_id}
GET /api/prompts/{id}/runs/{run_id}/stream
POST /api/prompts/{id}/runs/{run_id}/cancel
GET /api/prompts/{id}/versions
POST /api/prompts/{id}/versions/{version}/restore
POST /api/prompts/{id}/chain-canvas
A successful prompt deletion returns 204 No Content; clients must not expect
a response body.
The two POST stream routes return durable, sequenced SSE. Resume uses the run
stream with after_sequence or Last-Event-ID; cancellation is a separate
control request because closing an observer is not authority to stop work. The
run list merges owner-scoped interactive journal entries with Task runs whose
target is this prompt chain. A Task entry contains its Task/run pointers and
output references, while its full trace stays under the Task API.
Uploads
GET /api/uploads/config
POST /api/uploads
POST /api/uploads/multipart
PUT /api/uploads/multipart/{session}/parts/{n}
POST /api/uploads/multipart/{session}/complete
GET /api/uploads/multipart/{session}/complete
DELETE /api/uploads/multipart/{session}/result
DELETE /api/uploads/multipart/{session}
POST /api/uploads/stream
POST /api/uploads/index-batch
POST /api/uploads/attach
GET /api/uploads
GET /api/uploads/{id} | /status | /download | /text | /preview
PATCH /api/uploads/{id}
POST /api/uploads/{id}/index | /retry | /attach | /canvas | /export
DELETE /api/uploads/{id} | /attach
Multipart parts may upload concurrently. Completion and cancellation take an
exclusive transfer lock after in-flight parts finish, before recording durable
completion or abort authority. The completion journal then fences later part
mutations and stale owners; expiry alone cannot authorize cleanup.
A part PUT returns 429 upload_busy when the configured clusterwide
transaction budget or its API replica's local admission gate is full. The
DB-budget gate is acquired after buffering; eight separate body memory permits
per replica remain held through persistence. At most four belong to one verified
member across all their sessions; owner identity comes only from the authenticated
principal. The member counter is removed when its last admitted request settles.
A body has a 120-second idle deadline, refreshed only by nonempty chunks, and a
separate 30-minute hard receive deadline. Either returns 503 upload_timeout
with no provider side effect. The supported Uppy part retry path handles 5xx;
Desktop's part bridge preserves that HTTP status for the same retry policy.
Healthy progress can exceed the idle window while remaining within the hard bound. Retrying a buffered budget rejection resends the entire part
(up to 16 MiB). Repeated contention can exhaust the browser's bounded retry
sequence and fail the file. A rejected part has no provider side effect. The budget protects
connections held during the object-store call and ETag commit, while the
transfer lock continues to protect cancellation and completion ordering.
A registered document and its stored bytes survive session abort. A separate
result-discard action requires exact committed original provenance as described
below. Repeating completion replays the same owner-bound terminal outcome.
Uncertain publication retains its receipt and bytes; it does not grant abort or
a new transfer permission.
Replacing the same part is serialized: a concurrent replacement returns
upload_busy (429) before contacting storage, while different parts retain
concurrency. Each admitted attempt first commits an owner-scoped pending token
and removes its previous completion receipt, then reacquires its transaction
fences and verifies that token before contacting storage. A successful provider
reply and its authoritative receipt commit finish before caller cancellation
releases those fences. Completion checks the current receipts under its
exclusive fence before freezing the authoritative manifest in its durable
completion claim. Provider publication and hashing run outside that transaction;
subsequent settlement must still prove the same live claim.
An unconfirmed part write, including a lost acknowledgement or an interrupted
process, leaves its pending token in the existing session. The API refuses to
reuse that uncertain part or publish the upload (provider_unavailable, 503).
Neither an old receipt nor a provider listing resolves an unconfirmed
replacement; retain the owner-scoped session for operator inspection when its
provider disposition cannot be proved. A transient refusal before any provider
invocation may remove only that attempt's exact token and restore its previous
receipt if a shared transfer fence can be obtained immediately and no durable
terminal journal exists. Otherwise the pending token is retained.
This protocol requires a coordinated writer cutover: stop multipart admission
and drain all already admitted old writers before enabling the new writers.
Existing acknowledged receipts retain their positive-part/ETag JSON shape; the
reserved __kaizen_pending_parts namespace holds typed unresolved tokens and
is removed when its final token settles. Old readers reject that nonnumeric
namespace; old writers bypass attempt fencing and are unsafe during a mixed
writer rollout. A rollback also requires quiescing writers and resolving or
aborting pending sessions before starting old code. Live cutover and storage
provider qualification remain deployment checks; local tests do not establish
them.
Sessions have a 24-hour expiry and the scheduled worker inspects a bounded batch after an hour of quiet. The timer proves neither provider shutdown nor receipt disposability. Active, interrupted, uncertain, malformed and terminal completion receipts remain available for their original recovery handles. Only an acknowledged untouched-session abort or a validated rejected receipt grants the existing bounded cleanup authority; any exact-key reference, including foreign or deleted rows, preserves its bytes. Failed cleanup retains the receipt for a later attempt.
The web client sends raw files and multipart parts through browser XHR by
default. A Desktop host may advertise the desktop-upload-v1 bridge to carry
those byte bodies through its native transport; session creation, completion,
and cleanup still use the member API. Each part must return the server's ETag
before Uppy marks it complete. Desktop upload progress advances only after the
host confirms transfer, and aborting the request cancels its native transfer.
Multipart completion retries use the same session. A 503 response with
error.code set to upload_in_progress confirms that another completion owner
is still working; Kaizen polls completion every five seconds. It allows one
monotonic 630-second settlement budget, covering the server's 600-second
completion limit plus 30 seconds for a response. Each request is limited to
30 seconds or the remaining settlement budget, whichever is shorter.
Other transient network or provider failures receive at most three retries,
with delays of 750 milliseconds, two seconds, and five seconds. An authoritative
upload_in_progress response resets that transient retry allowance. A generic
503 or another status carrying the same code does not extend it.
Kaizen reports success only after a terminal completion receipt. If the total
budget expires, transient retries exhaust, or another completion error lacks
acknowledged terminal proof, it reports upload_completion_unknown while retaining
the original error as its cause. Status or error code alone does not prove that
the original publisher stopped. The Documents transfer
retains the exact original session and offers Check upload using only
GET /api/uploads/multipart/{original-session}/complete. This owner-bound GET
reads the exact matching committed journal outcome and document; it never acquires
or renews a claim, takes over an expired publisher, performs provider I/O or hash
work, cleans bytes, appends a receipt, commits, or queues ingestion. The original
acknowledged-storage publisher still uses POST. GET can observe late publication
by that original POST while preserving its duplicate origin. Aborted, malformed or missing results remain uncertain. A full original manifest
with no completion head, an interrupted attempt, or an expired active lease can
return 409 upload_resume_available with closed resume_session_id (the original
UUID) and resume_reason (unstarted, interrupted, or expired_active). This
read-only snapshot grants no ownership. The explicit Resume saving action
repeats GET, acknowledges its bound recovery handle in storage, then calls the
existing POST on that same UUID. Current POST locks, token/fingerprint fences and
terminal provenance remain decisive if a live owner or late result intervenes. A matching
committed rejection can return the same scoped rejection proof without cleanup.
An older backend returns 405 to GET, which retains the handle rather than
silently treating an unknown POST query flag as publication authority. It blocks Retry,
Cancel and Dismiss until that receipt confirms the outcome, including grouped
and programmatic retry. Repeated unknown, conflict or 404 responses never
authorize a new session or guessed document deletion. Cancel after completion begins still waits for settlement;
Kaizen does not abort the session or delete a document while publication is
unknown. Once confirmed, cancellation can remove only a document proven to have
been created by that session, leaving a pre-existing duplicate intact.
Cancellation retains the monotonic original-session handle until disposition is
acknowledged. DELETE /api/uploads/multipart/{original-session}/result accepts no
document selector: it validates the exact owned committed completion receipt and
derives the result UUID on the server. A duplicate result is never deleted.
Its closed response is { "session_id": "<original UUID>", "disposition": "discarded" | "duplicate_preserved" }. A gone duplicate grants no ACK. An
original new-document result can return discarded only after its row is
absent or tombstoned under the exact original owner. This is logical member
visibility (unretrievable and uncitable), not provider-purge completion. Existing
foreign-owner rows, including tombstones, fail closed. The underlying owner
deletion pipeline is unchanged.
Pure GET can return 409 upload_completion_discarded with
completion_outcome: "discarded" and completion_session_id: "<original UUID>"
only after validating committed original-new-document provenance plus exact
logical absence. This lets reload/lost-discard-ACK settle without another delete.
Generic 404/204, guessed document IDs, malformed proof, authentication failure
or uncertain journals never clear the handle. The browser checks closed proof
fields, original UUID and trusted app/API binding, refuses redirects, and keeps
Cancel recorded until storage removal is also acknowledged. Current frontend
and backend must be paired; an older backend fails safely with the retained
handle rather than substituting generic deletion or fresh-session Retry.
A rejected completion may add error.completion_outcome: "rejected" and
error.completion_session_id: "<original UUID>". These fields appear only after
an acknowledged, owner-bound terminal rejection for that exact session. They are
absent on authentication, missing results, fingerprint conflicts, provider
failures, failed persistence, or lost commit acknowledgements. The browser
accepts only the closed enum and exact original session binding with a terminal
rejection code; malformed or absent proof stays unresolved. Rejected proof can
permit a fresh Retry of the original in-memory file, but does not attest that
cleanup succeeded or authorize deleting uncertain bytes.
Before the first completion POST, the current browser stores and reads back a
version-1 sessionStorage handle containing only the opaque session ID, fixed
current app/API origin and path binding, and cancellation intent. It stores no
File, filename, content, credential, or owner identity. Each outcome-only Check validates and
persists the same handle again before completion I/O. Storage missing, malformed,
unavailable, or over quota fails closed; the first completion never starts.
Later storage faults do not erase an earlier handle or make Retry safe. Reload
or the existing 401 login redirect restores a dedicated Pending upload entry
with no File; its Check remains subject to current server owner/member checks.
Different app bindings cannot replay each other's handles. Auth denial, 404,
conflict and unknown responses retain the handle without abort, deletion or TTL
cleanup. Only an authoritative completion, acknowledged rejection, or proved owner
precompletion terminal abort clears it.
Remembered Cancel can delete only an authoritative duplicate=false result.
These recovery contracts require the current frontend/backend pairing. An older backend without acknowledged rejection proof leaves that completion unresolved; browser storage availability is a prerequisite for publication. No new environment variable or migration activates recovery.
Quiesce older upload, part and completion handlers before admitting current
cleanup. Abort checks the exact session key against every registered document,
including other owners and soft-deleted rows, before granting first or replayed
cleanup. A reference returns nonretryable 409 upload_abort_uncertain without
provider cleanup, session deletion or a new abort receipt. A retained uploading
session can reconcile its already-registered original through the same-session
completion path. The current locks do not fence older publishers that ignore
them; paired deployment alone is not proof that their in-flight requests drained.
A storage failure before any completion POST is distinct
recovery_storage_unavailable: no Check/replay can publish that known
preflight session. The original File and session ID remain in memory, and
ordinary Retry first validates restored storage. Precompletion cleanup is safe,
but a raw 204 is not owner proof because missing/foreign sessions also receive
it. Only an authenticated exact-owner precompletion abort after durable fencing,
provider cleanup and acknowledged row deletion adds
X-Kaizen-Multipart-Abort-Outcome: aborted and
X-Kaizen-Multipart-Abort-Session: <original UUID>. Both exact headers are required
to clear a possibly-written preflight handle. Missing/malformed/foreign headers,
409, provider failure, failed persistence or lost acknowledgements retain a
cleanup gap; they never establish terminal publication or permission to clear an
unknown completion. The current supported transport reads these headers through
same-origin browser fetch. Desktop/native transport and live cross-origin header
visibility have not been qualified; no bridge mapping or CORS permission is
invented, and invisible proof remains a gap.
A lost storage readback acknowledgement may leave a handle even though ZERO completion POSTs occurred. Storage and network dispatch are not atomic, so the handle is intent, not a persisted phase or proof of dispatch. After reload its no-File Pending row may inspect only the outcome-only GET; it cannot initiate completion, start/PUT, abort, delete, Retry or Dismiss. A missing outcome retains the original handle and recovery gap for operator inspection; no TTL clears it.
DELETE /api/uploads/multipart/{session} refuses a completed session with
non-retryable 409 upload_abort_completed, and interrupted or expired unknown
publication with non-retryable 409 upload_abort_uncertain. Only current active
lease contention returns retryable 503 upload_in_progress. Refusal preserves
the original receipt and bytes and never returns a false 204.
Large stream uploads persist a server-owned immutable intent before storage. Registration and the terminal receipt commit together under a publication fence. A rejection-receipt persistence failure retains a tracked settlement-pending intent and the original rejection. Recovery never silently publishes valid staged bytes after an unknown quota outcome. Operators can inspect pending receipt IDs and explicitly cancel a stranded stream through the bounded fenced recovery command described in Ingestion operations. Automatic owner recovery rotates fairly past uncertain receipts within its existing total budget; read-only operator inventory supports owner-validated receipt pagination.
Both index routes are backed by the same conditional claim, so neither can
report work it did not queue. POST /api/uploads/{id}/index answers 202 only
when a document actually entered dense_indexing; otherwise it raises
already_indexed (409) for a document already indexed or indexing,
index_unavailable (422) for one with nothing left to embed, and
validation_failed (422) while extraction is still running. The batch route
carries the same three states per document as already_indexed or ineligible
outcomes with the same reason text, because a partial batch cannot fail the
whole request.
POST /api/uploads/attach accepts { "conversation_id": "...", "document_ids": ["<uuid>", ...] } and persists an exact owner/global
selection under the authenticated member in one transaction. Unknown, deleted,
private foreign, or Project-only identifiers reject the entire operation;
none are silently omitted. Conversation attachments are rechecked for current
visibility on every turn. query_document retrieves citable chunks from the
whole authorized selection; no uploaded-document text is injected into the
prompt and no 20-file limit applies to retrieval turns.
The batch route marks conversation references as retrieval-only, including images. Model vision receives bytes only from the direct-upload path; a selected library image without extracted passages needs a fresh upload for visual analysis.
Artifacts
GET /api/artifacts
GET /api/artifacts/{id} | /versions
PATCH /api/artifacts/{id}
POST /api/artifacts/{id}/attach | /rename | /publish | /unpublish | /fork | /run
DELETE /api/artifacts/{id}
GET /api/artifacts/shared/{slug}
POST /api/artifacts/shared/{slug}/remix
GET /api/run-artifacts/{id}
GET /api/run-artifacts/{id}/preview
Artifact references returned in conversation, version, and attachment payloads
include the persisted optional language. Clients use it with kind=code to
restore the correct editor and sandbox run controls after a reload; non-code
artifacts return null.
PATCH /api/artifacts/{id} appends a manual version for an artifact the member
owns, using expected_version for optimistic concurrency. A governed
renderable artifact cannot be revised through this member endpoint: the request
returns HTTP 422 with error.code set to validation_failed, and no content,
title, or version change is committed. Only the separate server-attested
producer-result path can write another governed envelope.
POST /api/artifacts/{id}/rename,
POST /api/artifacts/{id}/publish, and
POST /api/artifacts/{id}/attach apply the same fail-closed rule to governed
renderable artifacts. Each returns HTTP 422 with error.code set to
validation_failed; rename leaves the attested title unchanged, publish
creates no share slug or workspace grant, and attach leaves the conversation
key unchanged. Owner resolution precedes this check, so a foreign root returns
HTTP 404 without revealing whether it is governed. Ordinary artifacts retain
all three operations. The unpublish route remains available to reduce access
to any governed result that already has an authorized share. A trusted producer
may create a governed result already scoped to its shared room; the member
attach endpoint cannot relocate an existing governed library root into another
conversation.
DELETE /api/artifacts/{id} removes every version of an artifact root its
caller owns, with one exception: a prompt-kind artifact is refused. That is the
kind a prompt chain's canvas is stored as, so opening a chain canvas creates a
stored artifact no member API call retires — deleting the chain itself leaves the
canvas in place. GET /api/artifacts omits prompt-kind artifacts, while direct
GET /api/artifacts/{id} can still read a canvas by the id returned when it was
opened. The delete refusal reuses the same HTTP 404 as a foreign root, so a
client must check the direct read to distinguish them.
Fork accepts only the optional from_version selector and ignores additional
JSON members. Remix has no request-body contract and, for compatibility, does
not parse a supplied body. Caller-supplied title, content, kind,
language, security, or other values never participate in either copy: the
server derives the title, content, kind, language, and lineage from the
authorized stored source version. This is also how fork and remix preserve a
governed envelope exactly without granting producer authority to the caller.
The run-artifact preview returns the same bounded DocumentPreviewPage shape
as document previews. Text is bounded directly; DOCX and XLSX return a safe
server-derived content projection; images and PDFs remain native. PPTX panes
fetch the authorized native content response and parse/render its OOXML
locally under bounded ZIP limits; the bytes are never sent to an external
viewer. The
private route repeats the owner ACL. In a collaborative chat the client uses
the resource preview-page and content routes instead, so current exact-room
access—not the producing member's artifact identifier—authorizes both preview
and download. Private conversation turns also project persisted
run_artifacts from their terminal tool rows, which restores native-file chips
in both the transcript and Workspace after a reload.
Invitation preflight synchronizes retained private-chat resources before it
computes the share manifest. That compatibility bridge includes exact-session
agent_artifact rows as run_artifact references, so files generated before a
chat became collaborative follow the same room authorization path as files
generated afterward.
Browser sessions
GET /api/browser-sessions?conversation_id={id}
POST /api/browser-sessions
DELETE /api/browser-sessions/{session_id}
WS /api/browser-sessions/{session_id}/socket
HTTP /api/browser-sessions/{session_id}/viewport/{path}
WS /api/browser-sessions/{session_id}/viewport/{path}
Gated on SANDBOX_BROWSER_SESSIONS_ENABLED, which ships off. With the config
store enabled, as shipped, the router stays mounted so a live change applies
without a restart, and a request gate answers every HTTP route with the same 404
as an absent surface while the flag is off, before any session or sandbox pool is
touched; a socket is refused. Without the config store the router is not
included at all.
GET discovers the conversation's live session. POST takes
{conversation_id, url} with unknown fields forbidden and returns
{session_id, url, viewport_url, private_actor_id, private}. Both calls resolve
conversation read access through the normal collaboration service.
DELETE is idempotent and restricted to the member who opened the session,
because it destroys the browser and its authenticated profile for everyone.
The /socket connection is a low-volume Kaizen control channel. It carries
navigation results, private-text state, and the captured screen size; browser
pixels and input do not use it. Browser-like member addresses are normalized to
HTTPS in the SPA before this channel sends them. Expected worker and network
failures return a bounded navigated refusal without closing the socket, and
that refusal carries the worker's own reason rather than the transport's — an
internal sandbox URL is not something to put in front of a member. Its close
codes remain authorization outcomes: 4404 for an unknown session, 4401 for
no usable forwarded identity, and 4403 for no conversation access.
A client sends {"type": "resize", "width", "height"} in device pixels: the
pane's CSS box multiplied by devicePixelRatio, which is what makes the stream
land one captured pixel per screen pixel instead of being rescaled. The worker
clamps the request, so a client cannot ask for an arbitrary screen. Only a
member who may drive may resize, because one screen serves the whole room, and
the applied size is broadcast to every viewer as resized.
The /viewport/ routes are an authenticated reverse proxy to the worker's
internal browser-media listener. The listener is never published directly.
Every HTTP request and WebSocket connection resolves the browser session and
conversation access before contacting the worker. Kaizen authentication cookies
and bearer credentials are not forwarded to the media adapter.
Browser-media signaling crosses the proxied WebSocket. Video and browser input
then use WebRTC through the operator-configured TURN service. The bundled
client forces iceTransportPolicy: "relay"; direct host and server-reflexive
paths are not candidates. Kaizen mints one short-lived TURN REST credential per
browser lease and passes only that credential to the worker.
The signaling hop is an allowlist, not a filter. The adapter runs with its own
member authentication disabled, so every adapter session it sees is an
administrator session, and its legacy bridge turns events such as admin/kick,
admin/lock, admin/mute, admin/control and broadcast/create into
privileged room operations. Kaizen therefore forwards only the events a viewer
needs — client/heartbeat, signal/offer, signal/answer,
signal/candidate, control/request, control/release, control/keyboard,
control/clipboard, screen/configurations, screen/resolution — and drops
everything else, including binary frames, which this protocol never uses in
that direction. screen/set stays out because one screen serves the whole
room, control/give because Kaizen decides who may drive, and chat and file
transfer because this product does not expose them. What the shipped client
offers is not the boundary: these frames are hand-writable on an authorized
socket. Raising the pinned adapter version means re-reading its bridge.
The HTTP hop is an allowlist by namespace. /viewport/ serves the adapter's
static client, and every path under the adapter's own api/ namespace is
refused there — the shipped bundle contains no reference to that API, so
relaying it buys nothing and costs the boundary. The check compares the path
Kaizen read against the path the adapter would route, so a request that only
resolves onto that namespace after decoding or dot-segment resolution is
refused too. Enumerating administrator paths instead is what this replaced: it
admitted POST api/room/screen, the screen reconfiguration screen/set is
kept off the socket allowlist to prevent, and api/login, which together with
a forwarded ?token= let a viewer mint an adapter credential.
Private text is enforced at both application and media boundaries. A viewer other than the private actor cannot open the browser-media proxy, and a connected signaling relay is closed within the guard interval. Closing the upstream signaling socket removes that peer and its media connection rather than merely hiding an iframe. The assistant's accessibility-tree and action routes remain refused while private text is active.
The assistant does not run another browser. A bundled Playwright bridge
connects over an inherited pipe to the exact headed Chromium the media adapter
captures. Reading the page is a read-capability tool available whenever a
session the caller may view exists: the member asked for the browser, so the
agent may look at what is on it. A read returns at most 400 accessibility
nodes per call and defaults to 200, and page text is wrapped as untrusted data
before it reaches the model. Acting is a sensitive tool with
requires_confirm, so every click, keystroke, scroll and navigation crosses the
member's confirmation gate individually and affects the same page every room
participant sees.
The member's latest message is read only for what it withholds. "Don't read it", "never summarize the page", "don't click anything" refuse that capability for the turn, terminally, so the model explains rather than retrying. Nothing else about the phrasing grants or removes access — inferring a request from wording was tried three times and measured against three adversarial corpora, and each version simultaneously leaked reads the member had refused and terminally refused ordinary asks, including every non-English one.
That pattern set is a backstop, not the boundary. It is English, so it will not recognize a refusal written in another language, and it cannot recognize every paraphrase; what honours those is the model, which has the member's own words and tool descriptions instructing it to obey a refusal however it is phrased. The patterns exist so the common forms are refused deterministically even when the model misreads them, and they are tuned to prefer over-refusing: the gap between a withholding and its verb admits only filler words, so a refusal aimed elsewhere ("don't worry, summarize the page") does not swallow the request, while emphasis ("never, ever read the page") does not defeat itself. The hard boundary for a page the agent must not read is private text, which refuses service-side and takes no account of phrasing at all.
Agents
GET /api/agents | /activity | /capability-catalog
POST /api/agents
GET /api/agents/{id} | /definition | /versions | /shares
PUT /api/agents/{id}
PATCH /api/agents/{id}/sharing
POST /api/agents/{id}/fork | /restore-version | /sandbox
PUT|DELETE /api/agents/{id}/shares[/{recipient_email}]
DELETE /api/agents/{id}
GET|PUT /api/agents/{id}/a2a
POST|DELETE /api/agents/{id}/a2a/keys[/{key_id}]
GET|PUT /api/agent-subagent-selection
GET|PUT /api/agent-routing-policy
GET /api/conversations/{id}/orchestrations/{oid}/trace
Studio proposals live under /api/studio/proposals.
GET /api/agents/capability-catalog resolves live tools under the caller's
forwarded identity and workroom. Its server_id and server_name fields are
compatibility names for a grouping surface: either a Core OpenAPI tag or an MCP
server; direct SDK adapters use the SDK surface. The response contains presentation metadata only, not operation
schemas, credentials, or platform records. Passing agent_id applies that
agent's read/write capability ceiling before returning the list; it does not
require an agent-authored SDK grant. Conversation input selection may narrow it
again but cannot widen it.
External capability routes exist only while
KAMIWAZA_EXTERNAL_CAPABILITIES_ENABLED is on:
GET|POST /api/capability-broker/sources
PUT|DELETE /api/capability-broker/sources/{source_id}
POST /api/capability-broker/execute-once
POST /api/capability-broker/credentials/remember
Source routes retain metadata pointers only and apply owner/workroom visibility;
only the owner may update or delete a shared source. Source create/update bodies
include allow_cross_origin, which must be explicitly true when schema_url
and api_base_url origins differ and is still bounded by the corresponding
admin setting. execute-once accepts a
short-lived signed plan plus browser-only secret fields and returns a bounded
endpoint result. It claims the nonce before egress and never persists the
fields. Query-auth plans are also bounded by their live admin setting.
credentials/remember additionally requires remember: true, the
remember feature switch, and a plan minted with remember permission; it returns
only credential_ref. Neither route accepts actor, role, workroom, endpoint,
method, or authorization policy from the request body.
Platform and workroom overview
GET /api/platform/workrooms
GET /api/workrooms/{workroom_id}/overview
The platform collection is a bounded, metadata-only inventory available to a
Kamiwaza Platform Admin. It does not grant access to workroom resources.
Workroom summaries include the optional configured level_id returned by Core.
The identifier is opaque: Kaizen does not infer a display label, a rank, or an
access grant from it. An unmarked workroom has no level identifier.
The older classification text is still returned beside level_id and is
deprecated: read level_id instead. Kaizen removes classification when
Kamiwaza 1.4 stops returning it. A Kamiwaza 1.3 build without workroom level
support returns only classification.
The overview accepts an explicit workroom identifier because platform
selection is presentation state, not authorization. Kaizen first resolves the
caller's live Core workroom list; a target absent from that list returns 403.
An authorized target is then header-scoped for read-only application,
extension, connector, and ingestion projections without calling the
session-mutating workroom-enter operation. Each family reports full,
partial, or unavailable, and responses exclude URLs, environment values,
Compose specifications, credentials, host details, and member content.
This request-local scoping is read-only. Kaizen never calls Core's session-mutating workroom-enter operation on a member's behalf. A write to a different authorized workroom remains unavailable until Core issues an operation-bound delegated workload capability for that target.
Tasks
GET /api/tasks
POST /api/tasks/{id}/activate | /pause | /resume | /test | /run-now
GET /api/tasks/{id} | /calendar | /flow | /runs
PATCH /api/tasks/{id}
DELETE /api/tasks/{id}
GET /api/tasks/calendar | /history | /history/grid
GET /api/task-runs/{run_id}
POST /api/task-runs/{run_id}/cancel | /retry
GET /api/task-runs/{run_id}/events | /events/stream | /events/{event_id}
GET /api/task-runs/{run_id}/output
GET /api/task-runs/{run_id}/corpus
POST /api/task-runs/{run_id}/corpus/pause | /resume
GET /api/task-runs/{run_id}/corpus/coverage
GET /api/task-runs/{run_id}/corpus/provenance?kind={kind}&id={stable_id}
GET /api/task-runs/{run_id}/corpus/graph?cursor={kind}:{stable_id}&limit={n}
GET /api/task-runs/{run_id}/corpus/exports/{export_id}
GET /api/task-runs/{run_id}/corpus/graph serves the run's complete graph in
keyset pages of up to 1,000 records, ordered by (record_kind, stable_id).
The response carries nodes, edges, next_cursor (null on the last page),
counts.records for the run, and revoked — the number of records this page
withheld because the member's access to a contributing source was revoked
after the run committed, or because the record carries no provenance at all.
Authorization is re-evaluated per page from the forwarded envelope; nothing in
the cursor or the query grants access, and an unparseable cursor returns the
first page rather than an error. An edge may reference an endpoint that a
later page carries, so a client holds unresolved edges until their nodes
arrive.
All Task routes share the server-side delegated-workload readiness gate. The capability projection exposes only an available boolean and closed safe reason categories; it never sends the browser component inventories, workload assertions, resource descriptors, grants, proofs, capabilities, or credentials. Retained owner-scoped definitions and history remain readable when readiness closes, while create, activate, run-now, retry, scheduler, queue, claim, model, credential, and protected-resource boundaries reject new work consistently.
GET /api/task-runs/{run_id}/output returns the run's member-visible output.
The run id selects a run; ownership is resolved from the forwarded envelope on
every read, so a caller who does not own the run receives the same
non-disclosing 404 as for an unknown run. The body carries a bounded answer
preview with answer_truncated, artifact handles pinned to the version the run
wrote, a provenance object naming the conversation to open, the journal
position, the attempt and the resolved target version, and an error object
with the same closed safe diagnosis the run detail exposes. state is one of
available, empty (with reason_category run_in_progress or
no_output_produced), or unavailable (with retention_expired or
output_source_unavailable). A failed run answers 200 with its error, not a
server error. The route never returns model reasoning, tool payloads, prompts,
credentials, provider errors, or another member's content.
Activation creates a Core consent intent from the authenticated member
session and, because the member's request to turn the task on is the consent,
approves that exact intent with the same live session. No consent page opens;
Kaizen keeps only the resulting opaque automation-grant reference and
correlation. The
request body cannot select sub, act, a workload revision or role, an
approval, a platform policy, or a credential. Declined, expired, replayed, or
CSRF-invalid consent leaves the definition inactive.
POST /api/tasks/{id}/activate returns status=active with the current task
detail once the intent is approved. If Core records the approval but has not
settled it, the call fails with 503 authority_unavailable and the draft stays
inactive; a refused intent answers the same way instead of a server error. When
Kaizen cannot approve the intent at all, the 503 names the cause:
consent_session_unavailable (the request carried no platform bearer),
consent_url_invalid (Core returned no usable one-time consent link), or
consent_request_failed (Core refused or failed the consent exchange; the
message names the step and the HTTP status or transport error class, never the
body, link, or a credential). A failed approval forgets that request's own
pending intent, so the next activation creates a fresh one, unless Core's
answer to the decision was lost or was a server error, in which case the
intent is kept for the next activation to read. An activation that meets a pending intent it did not
create answers 409 consent_pending until Core settles that intent or it
expires.
status=pending with a one-time HTTPS consent_url remains in the contract for
a platform that requires an interactive decision; clients must not invent or
cache a replacement URL, and the Tasks screen polls once on the
server-generated consent=complete return marker.
The member API continues to use Kaizen task and run IDs as its product contract. Opaque Core grant, run, claim, effect, resource, and credential-binding IDs are server-side correlation state; Core assertions, DPoP material, and capabilities never appear in JSON or SSE. Safe authority failures project bounded categories such as unavailable, incompatible, denied, fenced, cancelled, or ambiguous without raw platform/provider error text.
Task preview and create accept an optional strict input_template.corpus
object containing a folder, Collection, or explicit-document selector; graph or
record transform rules; JSONL/GraphML formats; and requested limits. Preview
returns only bounded aggregate selection counts and the server-capped effective
definition. Coverage and provenance page at 100 items or fewer, reauthorize
source detail at read time, and redact revoked sources. Export downloads are
owner-scoped streamed bytes with a content digest; they are not browser preview
payloads.
Chat background promotion is not a public Task-create route. The trusted agent
runtime calls the shared manage_tasks tool with run_in_background; Kaizen adds
the owner, accepted conversation and input, and exact selected Agent from
server context. The returned chat card reads the existing
GET /api/task-runs/{run_id} projection and uses the existing cancel route.
When a run publishes to its private task thread, output_refs.conversation_id
is the member-facing conversation key accepted by the conversation routes; the
internal task-thread UUID is never part of the member contract.
The same shared tool accepts an optional Goal for one-shot work and exposes a
separate create_loop operation for a Goal-backed, bounded Agent schedule.
update_goal is revision-fenced. steer_goal requires an exact live run, while
answer_goal_input requires both the exact run and pending input-request IDs.
For steering and answers, the server loads the current accepted member input;
the tool payload has no instruction-text field. GET /api/tasks/{id} projects
the safe Goal, current derived status, latest checkpoint reference, and pending
input request, while all private Goal and Agent-loop markers remain redacted.
Corpus pause and resume use the corpus controls above. Full output, trace,
usage, diagnosis, and artifacts remain available through the ordinary Task run
routes rather than a parallel chat-job API.
Preferences and notifications
GET|PUT /api/compression-preference
GET /api/compression-stats
POST /api/compression-stats/reset
GET|PUT /api/identity-icon
PUT /api/identity-avatar
GET /api/identity-avatar/{owner_sub}
DELETE /api/identity-avatar
GET /api/approval-preferences
PUT /api/approval-preferences/global
DELETE /api/approval-preferences/global
PUT /api/approval-preferences/agents/{agent_id}
DELETE /api/approval-preferences/agents/{agent_id}
GET /api/approvals
POST /api/approvals/{approval_id}/decision
GET /api/notifications
POST /api/notifications/read-all
POST /api/notifications/{id}/read
Approval preference writes accept one closed mode: default, ask_writes,
read_only, or never_ask. Selecting never_ask also requires
acknowledge_risk=true. Global deletion restores the workspace default; agent
deletion restores the personal default. A message-shaped conversation input
accepts an optional approval_mode turn selector and requires
approval_risk_acknowledged=true for never_ask. The server snapshots the
resolved mode into accepted execution authority. Approval
decisions accept approve or deny; the authenticated owner is always derived
from the platform envelope. remember_for_chat=true on an approved interactive
call appends a grant for that tool name under the same owner, conversation, and
agent; it does not change approval_mode. A task approval cannot be decided before its run
has durably entered awaiting_approval and released its lease. The approval
list returns the owner's recent requests across pending and terminal states so
the same conversation item can retain its decision after execution continues.
A pending item appears only after capacity release; a release failure may
appear as terminal context but is never exposed as approvable.
identity-icon is the member's own circle when it is not a picture. The body
is {"identity_icon", "identity_color"}, and any value present must come from
the closed catalog in kaizen.domain.member_identity, because the stored value
renders inside other members' browsers. A color may be sent alone, with
identity_icon null: that stores the member's letter on that color, which is
the ordinary case now that the symbol catalog is closed to new choices. A
symbol without a color is still rejected with 422, since a symbol needs a
color to sit on, and so is any value outside the catalog. Sending both as
null clears the row and restores the neutral initial letter, which is also
what an unset member reads.
The row is keyed on the caller's platform subject taken from the forwarded envelope, so there is no route through which one member can read or write another member's choice.
identity-avatar is the member's uploaded picture. PUT takes a multipart
body with one file field and accepts PNG, JPEG and WebP up to 4 MiB. The
server decodes with Pillow, center-crops to a square, and re-encodes to WebP at
512 and 128 pixels, so nothing a member uploads is served back to another
browser in its original encoding and no metadata inside the original survives.
It answers {"version", "updated_at"}. DELETE clears the caller's own
picture and answers {"version", "updated_at": null}.
Refusals are bounded: a type outside the three is unsupported_type (415),
a body over 4 MiB is file_too_large (413), and an image over 8192 pixels on
a side, an unreadable file, or an empty one is validation_failed (422).
Each carries field: "file". On the read, a size outside {128, 512} is
validation_failed with field: "size".
GET is readable by any authenticated member, because a circle is drawn for
co-members by definition. It answers the image bytes with
Cache-Control: public, max-age=31536000, immutable and an ETag carrying the
version, and size defaults to 128. A member with no picture and a subject
that names nobody answer the same 404 with the same body, so the route cannot
be used to discover who exists.
GET /api/auth/me projects the caller's own identity_icon,
identity_color, identity_avatar_owner and identity_avatar_version, and
the participant entries of the conversation detail and
/api/conversations/{conversation_id}/participants carry the same four fields
for each participant through the existing membership-authorized projection.
The picture's URL is the owner subject plus the version, so a member who
changes their picture invalidates every cached copy without a second endpoint
and without any cache being told.
Packages
GET /api/packages
Internal routes
/api/internal/pi/* are the agent-runtime callbacks — model completions,
decisions, tool execution, tool discovery, session and journal append,
compaction candidates. /api/internal/sandbox-packages/* is the package
broker.
/api/internal/sandbox-egress/public-key is the only one of these that is
deliberately unauthenticated:
GET /api/internal/sandbox-egress/public-key
→ 200 {"algorithm": "ed25519", "public_key": "<base64>"}
→ 503 {"error": {"code": "egress_unavailable"}}
The egress proxy reads it to verify the per-session grants Kaizen signs. It publishes a verifying key and nothing else — no fingerprint, no key id — because both are deterministic functions of a value derived from the deployment's field key, and an unauthenticated fingerprint is a stable deployment identifier and an offline oracle for confirming a guessed field key. The proxy computes its own from the material anyway. Holding the response confers no ability to mint a grant; the signing half never leaves the backend. A proxy pins the first key it sees for the life of the process, so answering this fetch with a substituted key is not a way in either.
These are not a public surface. They are the trusted boundary the runtime calls back through, and they carry their own authority checks. A delegated Task callback additionally proves the active Core run claim and fence, workload revision, immutable authority deadline, exact effect reservation, and sender-constrained capability before the shared model/tool pipeline accepts it.